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.3.0 — production release. Runs from PyPI (uvx local-wiki / uv tool install local-wiki), as a standalone executable (see Executable release), as a one-shot CLI, or as a global HTTP service.
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 serve --wiki-root <dir> --host 127.0.0.1 --port 8300 # 推荐(v1.3.0 子命令)
# 或旧形式(等价):
local-wiki --wiki-root <dir> --host 127.0.0.1 --port 8300
默认端点为 http://127.0.0.1:8300/mcp。--host 127.0.0.1 仅本机回环——MCP 无鉴权,勿绑 0.0.0.0 暴露公网。
Standalone executable (no Python required)
local-wiki.exe --wiki-root <dir>
Run (one-shot CLI — no service needed, v1.3.0)
Every wiki_* MCP tool has a CLI mirror. Each command builds its own Storage,
prints a single JSON document (ensure_ascii=False), and exits — nothing stays
running. --wiki-root defaults to the LOCAL_WIKI_ROOT env var.
LOCAL_WIKI_ROOT=C:/Users/Administrator/AppData/Local/hermes/wiki
local-wiki search nginx # → {"count": N, "matches": [...]}
local-wiki list --json # JSON always
local-wiki read global <rel>
local-wiki lint
local-wiki add "K8s 排障" --root global --keywords k8s,pod,集群 --content "..."
local-wiki commit # write commands honour the pending gate
local-wiki service status # Windows service management
local-wiki update --check # PyPI version check
Run (global HTTP service + lifecycle, v1.3.0)
uv tool install local-wiki # managed install (upgradeable)
local-wiki service install --start-now --wiki-root C:/Users/Administrator/AppData/Local/hermes/wiki
local-wiki service status # liveness = port + HTTP 200
local-wiki update --check # compare against PyPI
local-wiki update --wiki-root ... # upgrade + restart service
service / update 命令速查
local-wiki service install [--start-now] [--host 127.0.0.1] [--port 8300] [--wiki-root <root>]
local-wiki service start [--host] [--port] [--wiki-root <root>] # 立即拉起(端口探测就绪)
local-wiki service stop [--host] [--port] # 按端口找 PID 终止
local-wiki service status [--host] [--port] # 判活: schtasks 注册 + 端口/HTTP + serverInfo 版本
local-wiki service uninstall [--host] [--port] # 删任务/进程/文件
local-wiki update [--check] [--no-restart] [--host] [--port] [--wiki-root <root>]
并发与索引新鲜度
- 单进程收敛:所有档案经
url连同一个 HTTP 进程 → 单一Storage内存索引,多档案无内存索引竞争;写路径仍由 pending 队列 + OS 锁(LockFileEx)串行(MCP 库对 sync 工具用线程池执行,锁覆盖真多线程并发)。 - 索引自愈(ensure_fresh):CLI/外部进程直写磁盘后,服务端读路径按磁盘指纹(目录 mtime)自动重载索引——无需重启服务即可查到刚写的内容。
Wire into clients
Hermes (config.yaml) — standard PyPI run, no local source build
Single global HTTP service (recommended for multi-profile sharing):
mcp_servers:
wiki:
url: "http://127.0.0.1:8300/mcp" # Streamable HTTP, single global process
Or per-profile stdio:
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).- Empty-root direct create (1.2.0): a root with no pages and no root-level keywords (brand-new wiki-root or freshly created root) falls back to direct create instead of erroring — pointing
--wiki-rootat an empty directory and writing your first page just works, no setup step required. Non-empty roots keep the strict routing above (no silent misrouting). - 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 packaging/local-wiki.spec --distpath dist/exe
# → dist/exe/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.
Build env needs
typerinstalled (pip install typer) —collect_all('mcp')importsmcp.cli, which exits with "typer is required" if it's missing.
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
Metadata
Release files for local-wiki 1.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| local_wiki-1.3.0.tar.gz | 1.9 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| local_wiki-1.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 3.9 MB
Release files / local_wiki-1.3.0.tar.gz
| Download URL | local_wiki-1.3.0.tar.gz |
|---|---|
| Size | 1.9 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
40c3df5cc14567f2105b66cfce980548a43a6416906671b737fd00a6c25237ea
|
|
BLAKE2b-256 checksum How to use checksums |
38c715fa44d610a0e9c2e811e52a6c4acd4ec3280734d8317382e1a0793490d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / local_wiki-1.3.0-py3-none-any.whl
| Download URL | local_wiki-1.3.0-py3-none-any.whl |
|---|---|
| Size | 2.0 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
84f3a5deefd26cf3d0d7192f6748c982ab4fe0f3c671c18b8e8fa71d866b8357
|
|
BLAKE2b-256 checksum How to use checksums |
162f50e869b6ed3099ce18b511c0de14dfcb97fc0e0fe94f744f27877f95f1ed
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|