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
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.3.0.tar.gz.
File metadata
- Download URL: local_wiki-1.3.0.tar.gz
- Upload date:
- Size: 1.9 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
40c3df5cc14567f2105b66cfce980548a43a6416906671b737fd00a6c25237ea
|
|
| MD5 |
7df7d40c2040c7bf16deaf6bd7daefb8
|
|
| BLAKE2b-256 |
38c715fa44d610a0e9c2e811e52a6c4acd4ec3280734d8317382e1a0793490d5
|
File details
Details for the file local_wiki-1.3.0-py3-none-any.whl.
File metadata
- Download URL: local_wiki-1.3.0-py3-none-any.whl
- Upload date:
- Size: 2.0 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
84f3a5deefd26cf3d0d7192f6748c982ab4fe0f3c671c18b8e8fa71d866b8357
|
|
| MD5 |
3a2bfb157b74fc53dd1c2ca30bb67b44
|
|
| BLAKE2b-256 |
162f50e869b6ed3099ce18b511c0de14dfcb97fc0e0fe94f744f27877f95f1ed
|