local-wiki
Local-first agent wiki with caller-side AI conflict resolution. Stores knowledge as plain Markdown on your local filesystem, safe for multi-profile / multi-session concurrent writes via a pending-queue commit model — no torn writes, no silent overwrites.
Built as a standard MCP server, so it works with any MCP client (Hermes, Claude, Cursor, Codex...). Same code on any machine: pip install local-wiki or uvx local-wiki.
Version 1.0.0 — production release. Runs either from PyPI (uvx local-wiki) or as a standalone executable (see Executable release).
Why
- Skills should hold experience (how); knowledge (what) belongs in a wiki — this is the knowledge store for that.
- Naive file writes / single-writer tools have no concurrency control → multi-agent writes corrupt pages.
- This project fixes it with a pending queue: writes are staged (one file per draft in
pending/), a single committer serializes them under an OS lock, and conflicts are resolved by the caller (the agent) in its own conversation — the server itself never calls an LLM.
Design: AI resolution lives in the caller's conversation
The committer is intentionally dumb and deterministic (lock → hash check → atomic write → index update). It does not call any LLM:
wiki_writecreates new pages directly (op=create, target missing) or stages a draft intopending/<id>.json(recording the base-hash of the version the writer read).wiki_commitdrains the queue under a singleLockFileEx/flocklock (FIFO by file mtime).- No drift → commit directly.
- Drift (someone else committed meanwhile) → write a conflict record
pending/<id>.conflict.json(current + draft full text), keep the draft queued.
- The caller sees the conflict (
wiki_conflictsreturns both versions), merges them in its own conversation (the agent is the AI), and submits the result:wiki_resolve(id, side='merged', merged_content=...)→ apply your merge.- or
side='draft'/side='current'.
- Resolution removes the draft from the queue;
wiki_commitcan then drain the rest.
Result: zero LLM/key dependencies in the server, fully offline, and semantic merge quality comes from the caller's model — not from a hardcoded prompt.
Install
pip install local-wiki # or
uvx local-wiki --help # runs the latest PyPI release
Run (MCP server)
stdio (default)
local-wiki --wiki-root C:/path/to/wiki
HTTP (Streamable)
local-wiki --wiki-root C:/path/to/wiki --host 127.0.0.1 --port 8000
Standalone executable (no Python required)
local-wiki.exe --wiki-root C:/path/to/wiki
Wire into clients
Hermes (config.yaml) — standard PyPI run, no local source build
mcp_servers:
wiki:
command: uvx
args: ["local-wiki", "--wiki-root", "C:/Users/Administrator/AppData/Local/hermes/wiki"]
Hermes historically used
uvx --from <local-src-path>(build from source each launch). Since 1.0.0 the canonical setup is the PyPI package above; the standalone.execan be pointed to directly withcommand: <path>/local-wiki.exe.
Claude Code
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
MCP Tools (12)
| Tool | Description |
|---|---|
wiki_write(profile, rel, content, op, session?) |
Create (direct) or stage update/delete into the pending queue |
wiki_commit(once?) |
Serialize queue → commit; on drift write conflict record, keep draft queued |
wiki_conflicts() |
List open conflicts with full current+draft content for in-dialogue merge |
wiki_resolve(conflict_id, side, merged_content?) |
Apply draft / keep current / apply caller's merged content |
wiki_lint() |
Health check: index completeness, orphans, dead links, queue backlog, open conflicts, body length |
wiki_read(profile, rel) |
Read a page's full content (markdown with frontmatter) |
wiki_list(profile?) |
List page index entries (rel/title/updated/words/hash) |
wiki_search(query, profile?) |
Search in-memory index by rel/title/keywords (case-insensitive) |
wiki_index(profile?, action, rel?, keywords?) |
Browse & maintain index.json public keywords (read tree / update / delete) |
wiki_create_root(key, name, workdir?, type?, owner_profile?) |
Register a new wiki root (project shard) + root_index |
wiki_update_root(key, name?, workdir?, owner_profile?, type?) |
Update a root's meta |
wiki_delete_root(key, purge?) |
Unregister a root (purge=True deletes its folder, irreversible) |
Storage Layout (v1.0.0)
<wiki-root>/
├── index.json # root registry: {global + <project roots>} → meta
├── global/ # cross-profile knowledge (writable)
│ ├── pages/ index.json # public keywords per layer
├── <project-root>/ # one folder per registered project root
│ ├── pages/ index.json
├── pending/ # flat draft queue — one file per change
│ ├── <id>.json # draft record (target_path, op, content, base_hash)
│ ├── <id>.conflict.json # conflict record (current + draft)
│ ├── merge-log.md # AI merge audit trail
│ └── .lock # OS file lock (committer holds)
Layer naming — normalized to snake_case (v1.0.0)
Layer/root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.
smart-park/smartPark/SmartPark→smart_parkall_layersdedupes legacy hyphen profiles against registered roots (smart-park+smart_park→ onesmart_park)wiki_create_rootaccepts any spelling and stores the normalized key (must match[a-z0-9_]+after normalization)- Passing an alias (e.g.
smart-park) to read/write/search/update/delete works — it resolves to the canonical layer
Data model
Pages
Every page is Markdown with an optional YAML frontmatter; one is auto-added if missing (title = first heading, keywords: []).
---
title: Docker 使用
keywords: [特有kw]
---
<正文 body>
- Keywords are two-layered: public keywords live in each layer's
index.json(maintained viawiki_index, deleted with the page); a document's frontmatter keeps only its own unique keywords (duplicates of public ones are stripped on write, remaining total ≤ 60 chars). - Body length: must be < 10 000 tokens (DeepSeek-V4 official BPE tokenizer, offline, checked by
wiki_lint). - Invalid frontmatter (starts with
---and has a closing marker but fails YAML) is rejected on every write path.
Write semantics (v1.0.0 — hardened)
- create on a missing target → written directly (atomic), index updated.
- update on a missing target → rejected (
page not found). - update/delete/create-on-existing → staged into
pending/, committed serially. - Target vanished after enqueue (deleted externally between enqueue and commit):
update→ commit reportserror(never silently re-creates the page);create→ treated as a fresh create and written.
- All write paths normalize content through one validator (
_normalize_content): frontmatter validity, keyword strip/length, auto-add missing frontmatter — includingwiki_resolvemerged/draft, so bad content can never enter the wiki through conflict resolution.
Concurrency
- Cross-profile: physical sharding (
<root>/pages/) — different files, no collision. - Same-profile, multi-session: single committer + OS file lock serializes the queue.
- Atomic writes: temp-file + rename — readers never see partial state.
- Conflict: base-hash drift → both sides exposed; caller merges in-dialogue; nothing silently dropped.
- Delete drift (v0.3.8+): a delete based on a stale version surfaces as a conflict (
op=delete);resolve(side='draft')executes the delete,side='current'keeps the concurrent update.
Executable release
Since 1.0.0 a standalone Windows executable is built with PyInstaller (no Python/uvx needed):
# from repo root
pyinstaller --onefile --name local-wiki \
--collect-data mcp_server_wiki \
--collect-all mcp \
src/mcp_server_wiki/__main__.py
# → dist/local-wiki.exe
The tokenizer asset (assets/tokenizer.json) is bundled into the executable, so offline token counting works in the exe too.
Dev
pip install -e .[dev]
pytest # unit tests
python scripts/wiki_mcp_test.py # MCP stdio integration (12 tools + boundaries)
python scripts/wiki_concurrent_stress.py # multi-process lock stress
Companion skill
skills/local-wiki-usage/ ships the Hermes skill that teaches agents how to read/write the wiki correctly (project routing, pending-queue workflow, conflict resolution, pitfalls). Import it into Hermes (skills/public/local-wiki-usage/) for the guided workflow.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file local_wiki-1.0.0.tar.gz.
File metadata
- Download URL: local_wiki-1.0.0.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b9e9d95af6a2560b4ab370445a4cc37c6d1d2a9c57119f00fab4534fff8fc52
|
|
| MD5 |
ec9971788c3c7405f181a15b8cd500c6
|
|
| BLAKE2b-256 |
9a93affac54b3fa6df8229243e5ea5e14bb8067b8722e0ece31da28530d87f89
|
File details
Details for the file local_wiki-1.0.0-py3-none-any.whl.
File metadata
- Download URL: local_wiki-1.0.0-py3-none-any.whl
- Upload date:
- Size: 1.9 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f95e889b2beafdc50568c4910eca6cbf01c61479d7f9100e34a1a1922252199
|
|
| MD5 |
5b26baae7320831358cb4cd201962c28
|
|
| BLAKE2b-256 |
9a4923e4207a012956af80652d292b984883ca7adcccd2099b2c99ee05b9b089
|