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, OpenCode...). 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_addcreates new pages directly (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(conflict_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 <dir>
HTTP (Streamable)
local-wiki --wiki-root <dir> --host 127.0.0.1 --port 8000
Standalone executable (no Python required)
local-wiki.exe --wiki-root <dir>
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. The local-wiki dev setup in this repo (local-wiki-dev profile) instead runs the editable venv viapython -m mcp_server_wiki(see "OpenCode / Hermes local-wiki-dev" above) — source edits take effect on MCP restart, no PyPI/PyInstaller needed.
Claude Code
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
OpenCode / Hermes local-wiki-dev (this repo's dev setup)
The dev wiki MCP runs from the editable venv via python -m mcp_server_wiki (the reload switched off the local-wiki.exe launcher to the source module):
E:/wyd_work/local-wiki/.venv/Scripts/python.exe -m mcp_server_wiki --wiki-root E:/wyd_work/local-wiki/dev-wiki
The venv is an editable install of this repo, so source edits take effect after restarting the MCP server process — no PyInstaller rebuild needed.
MCP Tools (13)
| Tool | Description |
|---|---|
wiki_add(title, keywords=[], content="", root="") |
Unified knowledge write entry. Routes by title match → root-keyword intersection ≥5 → page-keyword intersection ≥5 → else {error}. New page = direct create; existing = staged to pending. Replaces the old wiki_write. |
wiki_delete(title, root="") |
Delete a page by normalized title; staged to pending. |
wiki_commit(once=False) |
Serialize pending 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_index(profile, action="read", rel="", keywords=[]) |
Maintain root-level keywords only. action='update' with rel omitted sets the root's category keywords; per-file keywords are no longer supported. |
wiki_search(query, profile="") |
Search the in-memory index by rel/title/keywords (case-insensitive). |
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_create_root(key, name, workdir="", type="project", owner_profile="") |
Register a new wiki root (project shard) + its index.json. |
wiki_update_root(key, name="", workdir=None, owner_profile="", type="") |
Update a root's meta. |
wiki_delete_root(key, purge=False) |
Unregister a root (purge=True deletes its folder, irreversible). |
Note:
wiki_writewas removed in the refactor — usewiki_add.profile(read/search/list) androot(write) are the same thing: the root key (e.g.global,smart_park).
Storage Layout (current)
<wiki-root>/
├── .wiki/
│ ├── global/ # cross-project knowledge (a normal root)
│ │ ├── <rel>.md # pages directly here (NO pages/ subdir)
│ │ └── index.json # {version, meta, keywords:[...]}
│ └── <root>/ # one folder per registered root
│ ├── <rel>.md
│ └── 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)
└── .lock # OS file lock (committer holds)
- Roots are registered by the existence of
.wiki/<dir>/index.json(directory name = root key). There is no top-level registry file. globalis just another root.index.jsonper root holds{version, meta, keywords:[...]}wherekeywordsis the root-level category list (a top-level array). Per-page keywords are NOT stored inindex.json— they live only in each page's frontmatter.
Root naming — normalized to snake_case
Root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.
smart-park/smartPark/SmartPark→smart_parkwiki_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 root
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: page frontmatter keywords are limited to a count (default 10; configurable via env
WIKI_KEYWORDS_MAX). Rootindex.jsonkeywords are category tags with no count limit. Invalid frontmatter is rejected on every write path. - 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 (current)
wiki_addon a missing target → written directly (atomic), index updated.wiki_add/wiki_deleteon an existing target → staged intopending/, committed serially bywiki_commit.wiki_addrouting when the target is ambiguous: title match → root-keyword intersection ≥5 → page-keyword intersection ≥5 → otherwise returns{error, hint}(not written; the caller may create a new root withwiki_create_root).- All write paths normalize content through one validator (
_normalize_content): frontmatter validity, keyword count/length, auto-add missing frontmatter — includingwiki_resolvemerged/draft, so bad content can never enter the wiki through conflict resolution.
Concurrency
- Cross-root: physical sharding (
.wiki/<root>/) — different files, no collision. - Same-root, 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: 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 build/local-wiki.spec
# → dist/local-wiki.exe
The tokenizer asset (src/mcp_server_wiki/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 (tests/test_*.py)
After editing src/, restart the running MCP server — a running process holds the old code and will not reflect your changes.
Companion skill
skills/local-wiki-usage/ ships the agent skill that teaches how to read/write the wiki correctly (write routing, pending-queue workflow, conflict resolution, pitfalls). It has been updated to the current wiki_add / .wiki/<root>/ API.
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.2.0.tar.gz.
File metadata
- Download URL: local_wiki-1.2.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 |
2f8cd37de07ec4cfe4eb2de0a45cadda0761ed6ca40154915444ac002de8116e
|
|
| MD5 |
880c4f17c1ff71f45ca7df8f208cf949
|
|
| BLAKE2b-256 |
c7af5fef3d9007a5eed8c9f739e808bf43b5b3dcbf3b6a8882a6e2519f251cad
|
File details
Details for the file local_wiki-1.2.0-py3-none-any.whl.
File metadata
- Download URL: local_wiki-1.2.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 |
cebefcaae54304e1eab144c2060bb3f8c6102f5c07ba49da5bb9fb793995a99e
|
|
| MD5 |
d85e48dd311a1b59921111cf1862a45b
|
|
| BLAKE2b-256 |
1625a9e5f149520944267d8c5cc7a23ffc9098976f3604c2a12b9f1276c847c3
|