local-wiki 1.0.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.
- local_wiki-1.0.0/LICENSE +21 -0
- local_wiki-1.0.0/PKG-INFO +205 -0
- local_wiki-1.0.0/README.md +184 -0
- local_wiki-1.0.0/pyproject.toml +34 -0
- local_wiki-1.0.0/setup.cfg +4 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/PKG-INFO +205 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/SOURCES.txt +16 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/dependency_links.txt +1 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/entry_points.txt +2 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/requires.txt +7 -0
- local_wiki-1.0.0/src/local_wiki.egg-info/top_level.txt +1 -0
- local_wiki-1.0.0/src/mcp_server_wiki/__init__.py +7 -0
- local_wiki-1.0.0/src/mcp_server_wiki/__main__.py +4 -0
- local_wiki-1.0.0/src/mcp_server_wiki/assets/tokenizer.json +267359 -0
- local_wiki-1.0.0/src/mcp_server_wiki/core.py +374 -0
- local_wiki-1.0.0/src/mcp_server_wiki/server.py +319 -0
- local_wiki-1.0.0/src/mcp_server_wiki/storage.py +650 -0
- local_wiki-1.0.0/tests/test_core.py +943 -0
local_wiki-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 warrior-kite
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: local-wiki
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Local-first agent wiki with caller-side AI conflict resolution — pending-queue commit model for safe multi-profile/multi-session writes
|
|
5
|
+
Author: warrior-kite
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: wiki,mcp,knowledge-base,agent,local-first,conflict-resolution
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: mcp>=1.0
|
|
15
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
Requires-Dist: tokenizers>=0.19
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# local-wiki
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
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`.
|
|
27
|
+
|
|
28
|
+
**Version 1.0.0** — production release. Runs either from PyPI (`uvx local-wiki`) or as a standalone executable (see [Executable release](#executable-release)).
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
- Skills should hold *experience* (how); **knowledge** (what) belongs in a wiki — this is the knowledge store for that.
|
|
33
|
+
- Naive file writes / single-writer tools have **no concurrency control** → multi-agent writes corrupt pages.
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
36
|
+
## Design: AI resolution lives in the caller's conversation
|
|
37
|
+
|
|
38
|
+
The committer is intentionally **dumb and deterministic** (lock → hash check → atomic write → index update). It does **not** call any LLM:
|
|
39
|
+
|
|
40
|
+
1. `wiki_write` creates new pages directly (`op=create`, target missing) or stages a draft into `pending/<id>.json` (recording the base-hash of the version the writer read).
|
|
41
|
+
2. `wiki_commit` drains the queue under a single `LockFileEx`/`flock` lock (FIFO by file mtime).
|
|
42
|
+
- **No drift** → commit directly.
|
|
43
|
+
- **Drift** (someone else committed meanwhile) → write a conflict record `pending/<id>.conflict.json` (current + draft full text), keep the draft queued.
|
|
44
|
+
3. The caller sees the conflict (`wiki_conflicts` returns both versions), **merges them in its own conversation** (the agent is the AI), and submits the result:
|
|
45
|
+
- `wiki_resolve(id, side='merged', merged_content=...)` → apply your merge.
|
|
46
|
+
- or `side='draft'` / `side='current'`.
|
|
47
|
+
4. Resolution removes the draft from the queue; `wiki_commit` can then drain the rest.
|
|
48
|
+
|
|
49
|
+
**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.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install local-wiki # or
|
|
55
|
+
uvx local-wiki --help # runs the latest PyPI release
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Run (MCP server)
|
|
59
|
+
|
|
60
|
+
### stdio (default)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
local-wiki --wiki-root C:/path/to/wiki
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### HTTP (Streamable)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
local-wiki --wiki-root C:/path/to/wiki --host 127.0.0.1 --port 8000
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Standalone executable (no Python required)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
local-wiki.exe --wiki-root C:/path/to/wiki
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Wire into clients
|
|
79
|
+
|
|
80
|
+
### Hermes (`config.yaml`) — standard PyPI run, no local source build
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
mcp_servers:
|
|
84
|
+
wiki:
|
|
85
|
+
command: uvx
|
|
86
|
+
args: ["local-wiki", "--wiki-root", "C:/Users/Administrator/AppData/Local/hermes/wiki"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> 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 `.exe` can be pointed to directly with `command: <path>/local-wiki.exe`.
|
|
90
|
+
|
|
91
|
+
### Claude Code
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## MCP Tools (12)
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `wiki_write(profile, rel, content, op, session?)` | Create (direct) or stage update/delete into the pending queue |
|
|
102
|
+
| `wiki_commit(once?)` | Serialize queue → commit; on drift write conflict record, keep draft queued |
|
|
103
|
+
| `wiki_conflicts()` | List open conflicts with full current+draft content for in-dialogue merge |
|
|
104
|
+
| `wiki_resolve(conflict_id, side, merged_content?)` | Apply draft / keep current / apply caller's merged content |
|
|
105
|
+
| `wiki_lint()` | Health check: index completeness, orphans, dead links, queue backlog, open conflicts, body length |
|
|
106
|
+
| `wiki_read(profile, rel)` | Read a page's full content (markdown with frontmatter) |
|
|
107
|
+
| `wiki_list(profile?)` | List page index entries (rel/title/updated/words/hash) |
|
|
108
|
+
| `wiki_search(query, profile?)` | Search in-memory index by rel/title/keywords (case-insensitive) |
|
|
109
|
+
| `wiki_index(profile?, action, rel?, keywords?)` | Browse & maintain `index.json` public keywords (read tree / update / delete) |
|
|
110
|
+
| `wiki_create_root(key, name, workdir?, type?, owner_profile?)` | Register a new wiki root (project shard) + root_index |
|
|
111
|
+
| `wiki_update_root(key, name?, workdir?, owner_profile?, type?)` | Update a root's meta |
|
|
112
|
+
| `wiki_delete_root(key, purge?)` | Unregister a root (purge=True deletes its folder, irreversible) |
|
|
113
|
+
|
|
114
|
+
## Storage Layout (v1.0.0)
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
<wiki-root>/
|
|
118
|
+
├── index.json # root registry: {global + <project roots>} → meta
|
|
119
|
+
├── global/ # cross-profile knowledge (writable)
|
|
120
|
+
│ ├── pages/ index.json # public keywords per layer
|
|
121
|
+
├── <project-root>/ # one folder per registered project root
|
|
122
|
+
│ ├── pages/ index.json
|
|
123
|
+
├── pending/ # flat draft queue — one file per change
|
|
124
|
+
│ ├── <id>.json # draft record (target_path, op, content, base_hash)
|
|
125
|
+
│ ├── <id>.conflict.json # conflict record (current + draft)
|
|
126
|
+
│ ├── merge-log.md # AI merge audit trail
|
|
127
|
+
│ └── .lock # OS file lock (committer holds)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Layer naming — normalized to snake_case (v1.0.0)
|
|
131
|
+
|
|
132
|
+
Layer/root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.
|
|
133
|
+
|
|
134
|
+
- `smart-park` / `smartPark` / `SmartPark` → `smart_park`
|
|
135
|
+
- `all_layers` dedupes legacy hyphen profiles against registered roots (`smart-park` + `smart_park` → one `smart_park`)
|
|
136
|
+
- `wiki_create_root` accepts any spelling and stores the normalized key (must match `[a-z0-9_]+` after normalization)
|
|
137
|
+
- Passing an alias (e.g. `smart-park`) to read/write/search/update/delete works — it resolves to the canonical layer
|
|
138
|
+
|
|
139
|
+
## Data model
|
|
140
|
+
|
|
141
|
+
### Pages
|
|
142
|
+
|
|
143
|
+
Every page is Markdown with an optional YAML frontmatter; one is auto-added if missing (title = first heading, `keywords: []`).
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
---
|
|
147
|
+
title: Docker 使用
|
|
148
|
+
keywords: [特有kw]
|
|
149
|
+
---
|
|
150
|
+
<正文 body>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **Keywords are two-layered**: public keywords live in each layer's `index.json` (maintained via `wiki_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).
|
|
154
|
+
- **Body length**: must be < 10 000 tokens (DeepSeek-V4 official BPE tokenizer, offline, checked by `wiki_lint`).
|
|
155
|
+
- **Invalid frontmatter** (starts with `---` and has a closing marker but fails YAML) is rejected on every write path.
|
|
156
|
+
|
|
157
|
+
### Write semantics (v1.0.0 — hardened)
|
|
158
|
+
|
|
159
|
+
- **create** on a missing target → written directly (atomic), index updated.
|
|
160
|
+
- **update** on a missing target → rejected (`page not found`).
|
|
161
|
+
- **update/delete/create-on-existing** → staged into `pending/`, committed serially.
|
|
162
|
+
- **Target vanished after enqueue** (deleted externally between enqueue and commit):
|
|
163
|
+
- `update` → commit reports `error` (never silently re-creates the page);
|
|
164
|
+
- `create` → treated as a fresh create and written.
|
|
165
|
+
- **All write paths normalize content** through one validator (`_normalize_content`): frontmatter validity, keyword strip/length, auto-add missing frontmatter — **including `wiki_resolve` merged/draft**, so bad content can never enter the wiki through conflict resolution.
|
|
166
|
+
|
|
167
|
+
### Concurrency
|
|
168
|
+
|
|
169
|
+
- **Cross-profile**: physical sharding (`<root>/pages/`) — different files, no collision.
|
|
170
|
+
- **Same-profile, multi-session**: single committer + OS file lock serializes the queue.
|
|
171
|
+
- **Atomic writes**: temp-file + rename — readers never see partial state.
|
|
172
|
+
- **Conflict**: base-hash drift → both sides exposed; caller merges in-dialogue; nothing silently dropped.
|
|
173
|
+
- **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.
|
|
174
|
+
|
|
175
|
+
## Executable release
|
|
176
|
+
|
|
177
|
+
Since 1.0.0 a standalone Windows executable is built with PyInstaller (no Python/uvx needed):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# from repo root
|
|
181
|
+
pyinstaller --onefile --name local-wiki \
|
|
182
|
+
--collect-data mcp_server_wiki \
|
|
183
|
+
--collect-all mcp \
|
|
184
|
+
src/mcp_server_wiki/__main__.py
|
|
185
|
+
# → dist/local-wiki.exe
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The tokenizer asset (`assets/tokenizer.json`) is bundled into the executable, so offline token counting works in the exe too.
|
|
189
|
+
|
|
190
|
+
## Dev
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e .[dev]
|
|
194
|
+
pytest # unit tests
|
|
195
|
+
python scripts/wiki_mcp_test.py # MCP stdio integration (12 tools + boundaries)
|
|
196
|
+
python scripts/wiki_concurrent_stress.py # multi-process lock stress
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Companion skill
|
|
200
|
+
|
|
201
|
+
`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.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# local-wiki
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
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`.
|
|
6
|
+
|
|
7
|
+
**Version 1.0.0** — production release. Runs either from PyPI (`uvx local-wiki`) or as a standalone executable (see [Executable release](#executable-release)).
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
- Skills should hold *experience* (how); **knowledge** (what) belongs in a wiki — this is the knowledge store for that.
|
|
12
|
+
- Naive file writes / single-writer tools have **no concurrency control** → multi-agent writes corrupt pages.
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
15
|
+
## Design: AI resolution lives in the caller's conversation
|
|
16
|
+
|
|
17
|
+
The committer is intentionally **dumb and deterministic** (lock → hash check → atomic write → index update). It does **not** call any LLM:
|
|
18
|
+
|
|
19
|
+
1. `wiki_write` creates new pages directly (`op=create`, target missing) or stages a draft into `pending/<id>.json` (recording the base-hash of the version the writer read).
|
|
20
|
+
2. `wiki_commit` drains the queue under a single `LockFileEx`/`flock` lock (FIFO by file mtime).
|
|
21
|
+
- **No drift** → commit directly.
|
|
22
|
+
- **Drift** (someone else committed meanwhile) → write a conflict record `pending/<id>.conflict.json` (current + draft full text), keep the draft queued.
|
|
23
|
+
3. The caller sees the conflict (`wiki_conflicts` returns both versions), **merges them in its own conversation** (the agent is the AI), and submits the result:
|
|
24
|
+
- `wiki_resolve(id, side='merged', merged_content=...)` → apply your merge.
|
|
25
|
+
- or `side='draft'` / `side='current'`.
|
|
26
|
+
4. Resolution removes the draft from the queue; `wiki_commit` can then drain the rest.
|
|
27
|
+
|
|
28
|
+
**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.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install local-wiki # or
|
|
34
|
+
uvx local-wiki --help # runs the latest PyPI release
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Run (MCP server)
|
|
38
|
+
|
|
39
|
+
### stdio (default)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
local-wiki --wiki-root C:/path/to/wiki
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### HTTP (Streamable)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
local-wiki --wiki-root C:/path/to/wiki --host 127.0.0.1 --port 8000
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Standalone executable (no Python required)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
local-wiki.exe --wiki-root C:/path/to/wiki
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Wire into clients
|
|
58
|
+
|
|
59
|
+
### Hermes (`config.yaml`) — standard PyPI run, no local source build
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
mcp_servers:
|
|
63
|
+
wiki:
|
|
64
|
+
command: uvx
|
|
65
|
+
args: ["local-wiki", "--wiki-root", "C:/Users/Administrator/AppData/Local/hermes/wiki"]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
> 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 `.exe` can be pointed to directly with `command: <path>/local-wiki.exe`.
|
|
69
|
+
|
|
70
|
+
### Claude Code
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## MCP Tools (12)
|
|
77
|
+
|
|
78
|
+
| Tool | Description |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `wiki_write(profile, rel, content, op, session?)` | Create (direct) or stage update/delete into the pending queue |
|
|
81
|
+
| `wiki_commit(once?)` | Serialize queue → commit; on drift write conflict record, keep draft queued |
|
|
82
|
+
| `wiki_conflicts()` | List open conflicts with full current+draft content for in-dialogue merge |
|
|
83
|
+
| `wiki_resolve(conflict_id, side, merged_content?)` | Apply draft / keep current / apply caller's merged content |
|
|
84
|
+
| `wiki_lint()` | Health check: index completeness, orphans, dead links, queue backlog, open conflicts, body length |
|
|
85
|
+
| `wiki_read(profile, rel)` | Read a page's full content (markdown with frontmatter) |
|
|
86
|
+
| `wiki_list(profile?)` | List page index entries (rel/title/updated/words/hash) |
|
|
87
|
+
| `wiki_search(query, profile?)` | Search in-memory index by rel/title/keywords (case-insensitive) |
|
|
88
|
+
| `wiki_index(profile?, action, rel?, keywords?)` | Browse & maintain `index.json` public keywords (read tree / update / delete) |
|
|
89
|
+
| `wiki_create_root(key, name, workdir?, type?, owner_profile?)` | Register a new wiki root (project shard) + root_index |
|
|
90
|
+
| `wiki_update_root(key, name?, workdir?, owner_profile?, type?)` | Update a root's meta |
|
|
91
|
+
| `wiki_delete_root(key, purge?)` | Unregister a root (purge=True deletes its folder, irreversible) |
|
|
92
|
+
|
|
93
|
+
## Storage Layout (v1.0.0)
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
<wiki-root>/
|
|
97
|
+
├── index.json # root registry: {global + <project roots>} → meta
|
|
98
|
+
├── global/ # cross-profile knowledge (writable)
|
|
99
|
+
│ ├── pages/ index.json # public keywords per layer
|
|
100
|
+
├── <project-root>/ # one folder per registered project root
|
|
101
|
+
│ ├── pages/ index.json
|
|
102
|
+
├── pending/ # flat draft queue — one file per change
|
|
103
|
+
│ ├── <id>.json # draft record (target_path, op, content, base_hash)
|
|
104
|
+
│ ├── <id>.conflict.json # conflict record (current + draft)
|
|
105
|
+
│ ├── merge-log.md # AI merge audit trail
|
|
106
|
+
│ └── .lock # OS file lock (committer holds)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Layer naming — normalized to snake_case (v1.0.0)
|
|
110
|
+
|
|
111
|
+
Layer/root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.
|
|
112
|
+
|
|
113
|
+
- `smart-park` / `smartPark` / `SmartPark` → `smart_park`
|
|
114
|
+
- `all_layers` dedupes legacy hyphen profiles against registered roots (`smart-park` + `smart_park` → one `smart_park`)
|
|
115
|
+
- `wiki_create_root` accepts any spelling and stores the normalized key (must match `[a-z0-9_]+` after normalization)
|
|
116
|
+
- Passing an alias (e.g. `smart-park`) to read/write/search/update/delete works — it resolves to the canonical layer
|
|
117
|
+
|
|
118
|
+
## Data model
|
|
119
|
+
|
|
120
|
+
### Pages
|
|
121
|
+
|
|
122
|
+
Every page is Markdown with an optional YAML frontmatter; one is auto-added if missing (title = first heading, `keywords: []`).
|
|
123
|
+
|
|
124
|
+
```markdown
|
|
125
|
+
---
|
|
126
|
+
title: Docker 使用
|
|
127
|
+
keywords: [特有kw]
|
|
128
|
+
---
|
|
129
|
+
<正文 body>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- **Keywords are two-layered**: public keywords live in each layer's `index.json` (maintained via `wiki_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).
|
|
133
|
+
- **Body length**: must be < 10 000 tokens (DeepSeek-V4 official BPE tokenizer, offline, checked by `wiki_lint`).
|
|
134
|
+
- **Invalid frontmatter** (starts with `---` and has a closing marker but fails YAML) is rejected on every write path.
|
|
135
|
+
|
|
136
|
+
### Write semantics (v1.0.0 — hardened)
|
|
137
|
+
|
|
138
|
+
- **create** on a missing target → written directly (atomic), index updated.
|
|
139
|
+
- **update** on a missing target → rejected (`page not found`).
|
|
140
|
+
- **update/delete/create-on-existing** → staged into `pending/`, committed serially.
|
|
141
|
+
- **Target vanished after enqueue** (deleted externally between enqueue and commit):
|
|
142
|
+
- `update` → commit reports `error` (never silently re-creates the page);
|
|
143
|
+
- `create` → treated as a fresh create and written.
|
|
144
|
+
- **All write paths normalize content** through one validator (`_normalize_content`): frontmatter validity, keyword strip/length, auto-add missing frontmatter — **including `wiki_resolve` merged/draft**, so bad content can never enter the wiki through conflict resolution.
|
|
145
|
+
|
|
146
|
+
### Concurrency
|
|
147
|
+
|
|
148
|
+
- **Cross-profile**: physical sharding (`<root>/pages/`) — different files, no collision.
|
|
149
|
+
- **Same-profile, multi-session**: single committer + OS file lock serializes the queue.
|
|
150
|
+
- **Atomic writes**: temp-file + rename — readers never see partial state.
|
|
151
|
+
- **Conflict**: base-hash drift → both sides exposed; caller merges in-dialogue; nothing silently dropped.
|
|
152
|
+
- **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.
|
|
153
|
+
|
|
154
|
+
## Executable release
|
|
155
|
+
|
|
156
|
+
Since 1.0.0 a standalone Windows executable is built with PyInstaller (no Python/uvx needed):
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# from repo root
|
|
160
|
+
pyinstaller --onefile --name local-wiki \
|
|
161
|
+
--collect-data mcp_server_wiki \
|
|
162
|
+
--collect-all mcp \
|
|
163
|
+
src/mcp_server_wiki/__main__.py
|
|
164
|
+
# → dist/local-wiki.exe
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The tokenizer asset (`assets/tokenizer.json`) is bundled into the executable, so offline token counting works in the exe too.
|
|
168
|
+
|
|
169
|
+
## Dev
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
pip install -e .[dev]
|
|
173
|
+
pytest # unit tests
|
|
174
|
+
python scripts/wiki_mcp_test.py # MCP stdio integration (12 tools + boundaries)
|
|
175
|
+
python scripts/wiki_concurrent_stress.py # multi-process lock stress
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Companion skill
|
|
179
|
+
|
|
180
|
+
`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.
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "local-wiki"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Local-first agent wiki with caller-side AI conflict resolution — pending-queue commit model for safe multi-profile/multi-session writes"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "warrior-kite" }]
|
|
9
|
+
keywords = ["wiki", "mcp", "knowledge-base", "agent", "local-first", "conflict-resolution"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Programming Language :: Python :: 3",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
]
|
|
15
|
+
dependencies = ["mcp>=1.0", "PyYAML>=6.0", "tokenizers>=0.19"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
local-wiki = "mcp_server_wiki.__main__:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["pytest>=8.0", "build>=1.0"]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["setuptools>=68"]
|
|
25
|
+
build-backend = "setuptools.build_meta"
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.package-data]
|
|
31
|
+
mcp_server_wiki = ["assets/*.json"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: local-wiki
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Local-first agent wiki with caller-side AI conflict resolution — pending-queue commit model for safe multi-profile/multi-session writes
|
|
5
|
+
Author: warrior-kite
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: wiki,mcp,knowledge-base,agent,local-first,conflict-resolution
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: mcp>=1.0
|
|
15
|
+
Requires-Dist: PyYAML>=6.0
|
|
16
|
+
Requires-Dist: tokenizers>=0.19
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
19
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# local-wiki
|
|
23
|
+
|
|
24
|
+
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.
|
|
25
|
+
|
|
26
|
+
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`.
|
|
27
|
+
|
|
28
|
+
**Version 1.0.0** — production release. Runs either from PyPI (`uvx local-wiki`) or as a standalone executable (see [Executable release](#executable-release)).
|
|
29
|
+
|
|
30
|
+
## Why
|
|
31
|
+
|
|
32
|
+
- Skills should hold *experience* (how); **knowledge** (what) belongs in a wiki — this is the knowledge store for that.
|
|
33
|
+
- Naive file writes / single-writer tools have **no concurrency control** → multi-agent writes corrupt pages.
|
|
34
|
+
- 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.
|
|
35
|
+
|
|
36
|
+
## Design: AI resolution lives in the caller's conversation
|
|
37
|
+
|
|
38
|
+
The committer is intentionally **dumb and deterministic** (lock → hash check → atomic write → index update). It does **not** call any LLM:
|
|
39
|
+
|
|
40
|
+
1. `wiki_write` creates new pages directly (`op=create`, target missing) or stages a draft into `pending/<id>.json` (recording the base-hash of the version the writer read).
|
|
41
|
+
2. `wiki_commit` drains the queue under a single `LockFileEx`/`flock` lock (FIFO by file mtime).
|
|
42
|
+
- **No drift** → commit directly.
|
|
43
|
+
- **Drift** (someone else committed meanwhile) → write a conflict record `pending/<id>.conflict.json` (current + draft full text), keep the draft queued.
|
|
44
|
+
3. The caller sees the conflict (`wiki_conflicts` returns both versions), **merges them in its own conversation** (the agent is the AI), and submits the result:
|
|
45
|
+
- `wiki_resolve(id, side='merged', merged_content=...)` → apply your merge.
|
|
46
|
+
- or `side='draft'` / `side='current'`.
|
|
47
|
+
4. Resolution removes the draft from the queue; `wiki_commit` can then drain the rest.
|
|
48
|
+
|
|
49
|
+
**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.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install local-wiki # or
|
|
55
|
+
uvx local-wiki --help # runs the latest PyPI release
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Run (MCP server)
|
|
59
|
+
|
|
60
|
+
### stdio (default)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
local-wiki --wiki-root C:/path/to/wiki
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### HTTP (Streamable)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
local-wiki --wiki-root C:/path/to/wiki --host 127.0.0.1 --port 8000
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Standalone executable (no Python required)
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
local-wiki.exe --wiki-root C:/path/to/wiki
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Wire into clients
|
|
79
|
+
|
|
80
|
+
### Hermes (`config.yaml`) — standard PyPI run, no local source build
|
|
81
|
+
|
|
82
|
+
```yaml
|
|
83
|
+
mcp_servers:
|
|
84
|
+
wiki:
|
|
85
|
+
command: uvx
|
|
86
|
+
args: ["local-wiki", "--wiki-root", "C:/Users/Administrator/AppData/Local/hermes/wiki"]
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
> 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 `.exe` can be pointed to directly with `command: <path>/local-wiki.exe`.
|
|
90
|
+
|
|
91
|
+
### Claude Code
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## MCP Tools (12)
|
|
98
|
+
|
|
99
|
+
| Tool | Description |
|
|
100
|
+
|---|---|
|
|
101
|
+
| `wiki_write(profile, rel, content, op, session?)` | Create (direct) or stage update/delete into the pending queue |
|
|
102
|
+
| `wiki_commit(once?)` | Serialize queue → commit; on drift write conflict record, keep draft queued |
|
|
103
|
+
| `wiki_conflicts()` | List open conflicts with full current+draft content for in-dialogue merge |
|
|
104
|
+
| `wiki_resolve(conflict_id, side, merged_content?)` | Apply draft / keep current / apply caller's merged content |
|
|
105
|
+
| `wiki_lint()` | Health check: index completeness, orphans, dead links, queue backlog, open conflicts, body length |
|
|
106
|
+
| `wiki_read(profile, rel)` | Read a page's full content (markdown with frontmatter) |
|
|
107
|
+
| `wiki_list(profile?)` | List page index entries (rel/title/updated/words/hash) |
|
|
108
|
+
| `wiki_search(query, profile?)` | Search in-memory index by rel/title/keywords (case-insensitive) |
|
|
109
|
+
| `wiki_index(profile?, action, rel?, keywords?)` | Browse & maintain `index.json` public keywords (read tree / update / delete) |
|
|
110
|
+
| `wiki_create_root(key, name, workdir?, type?, owner_profile?)` | Register a new wiki root (project shard) + root_index |
|
|
111
|
+
| `wiki_update_root(key, name?, workdir?, owner_profile?, type?)` | Update a root's meta |
|
|
112
|
+
| `wiki_delete_root(key, purge?)` | Unregister a root (purge=True deletes its folder, irreversible) |
|
|
113
|
+
|
|
114
|
+
## Storage Layout (v1.0.0)
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
<wiki-root>/
|
|
118
|
+
├── index.json # root registry: {global + <project roots>} → meta
|
|
119
|
+
├── global/ # cross-profile knowledge (writable)
|
|
120
|
+
│ ├── pages/ index.json # public keywords per layer
|
|
121
|
+
├── <project-root>/ # one folder per registered project root
|
|
122
|
+
│ ├── pages/ index.json
|
|
123
|
+
├── pending/ # flat draft queue — one file per change
|
|
124
|
+
│ ├── <id>.json # draft record (target_path, op, content, base_hash)
|
|
125
|
+
│ ├── <id>.conflict.json # conflict record (current + draft)
|
|
126
|
+
│ ├── merge-log.md # AI merge audit trail
|
|
127
|
+
│ └── .lock # OS file lock (committer holds)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Layer naming — normalized to snake_case (v1.0.0)
|
|
131
|
+
|
|
132
|
+
Layer/root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.
|
|
133
|
+
|
|
134
|
+
- `smart-park` / `smartPark` / `SmartPark` → `smart_park`
|
|
135
|
+
- `all_layers` dedupes legacy hyphen profiles against registered roots (`smart-park` + `smart_park` → one `smart_park`)
|
|
136
|
+
- `wiki_create_root` accepts any spelling and stores the normalized key (must match `[a-z0-9_]+` after normalization)
|
|
137
|
+
- Passing an alias (e.g. `smart-park`) to read/write/search/update/delete works — it resolves to the canonical layer
|
|
138
|
+
|
|
139
|
+
## Data model
|
|
140
|
+
|
|
141
|
+
### Pages
|
|
142
|
+
|
|
143
|
+
Every page is Markdown with an optional YAML frontmatter; one is auto-added if missing (title = first heading, `keywords: []`).
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
---
|
|
147
|
+
title: Docker 使用
|
|
148
|
+
keywords: [特有kw]
|
|
149
|
+
---
|
|
150
|
+
<正文 body>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **Keywords are two-layered**: public keywords live in each layer's `index.json` (maintained via `wiki_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).
|
|
154
|
+
- **Body length**: must be < 10 000 tokens (DeepSeek-V4 official BPE tokenizer, offline, checked by `wiki_lint`).
|
|
155
|
+
- **Invalid frontmatter** (starts with `---` and has a closing marker but fails YAML) is rejected on every write path.
|
|
156
|
+
|
|
157
|
+
### Write semantics (v1.0.0 — hardened)
|
|
158
|
+
|
|
159
|
+
- **create** on a missing target → written directly (atomic), index updated.
|
|
160
|
+
- **update** on a missing target → rejected (`page not found`).
|
|
161
|
+
- **update/delete/create-on-existing** → staged into `pending/`, committed serially.
|
|
162
|
+
- **Target vanished after enqueue** (deleted externally between enqueue and commit):
|
|
163
|
+
- `update` → commit reports `error` (never silently re-creates the page);
|
|
164
|
+
- `create` → treated as a fresh create and written.
|
|
165
|
+
- **All write paths normalize content** through one validator (`_normalize_content`): frontmatter validity, keyword strip/length, auto-add missing frontmatter — **including `wiki_resolve` merged/draft**, so bad content can never enter the wiki through conflict resolution.
|
|
166
|
+
|
|
167
|
+
### Concurrency
|
|
168
|
+
|
|
169
|
+
- **Cross-profile**: physical sharding (`<root>/pages/`) — different files, no collision.
|
|
170
|
+
- **Same-profile, multi-session**: single committer + OS file lock serializes the queue.
|
|
171
|
+
- **Atomic writes**: temp-file + rename — readers never see partial state.
|
|
172
|
+
- **Conflict**: base-hash drift → both sides exposed; caller merges in-dialogue; nothing silently dropped.
|
|
173
|
+
- **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.
|
|
174
|
+
|
|
175
|
+
## Executable release
|
|
176
|
+
|
|
177
|
+
Since 1.0.0 a standalone Windows executable is built with PyInstaller (no Python/uvx needed):
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# from repo root
|
|
181
|
+
pyinstaller --onefile --name local-wiki \
|
|
182
|
+
--collect-data mcp_server_wiki \
|
|
183
|
+
--collect-all mcp \
|
|
184
|
+
src/mcp_server_wiki/__main__.py
|
|
185
|
+
# → dist/local-wiki.exe
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The tokenizer asset (`assets/tokenizer.json`) is bundled into the executable, so offline token counting works in the exe too.
|
|
189
|
+
|
|
190
|
+
## Dev
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
pip install -e .[dev]
|
|
194
|
+
pytest # unit tests
|
|
195
|
+
python scripts/wiki_mcp_test.py # MCP stdio integration (12 tools + boundaries)
|
|
196
|
+
python scripts/wiki_concurrent_stress.py # multi-process lock stress
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Companion skill
|
|
200
|
+
|
|
201
|
+
`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.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/local_wiki.egg-info/PKG-INFO
|
|
5
|
+
src/local_wiki.egg-info/SOURCES.txt
|
|
6
|
+
src/local_wiki.egg-info/dependency_links.txt
|
|
7
|
+
src/local_wiki.egg-info/entry_points.txt
|
|
8
|
+
src/local_wiki.egg-info/requires.txt
|
|
9
|
+
src/local_wiki.egg-info/top_level.txt
|
|
10
|
+
src/mcp_server_wiki/__init__.py
|
|
11
|
+
src/mcp_server_wiki/__main__.py
|
|
12
|
+
src/mcp_server_wiki/core.py
|
|
13
|
+
src/mcp_server_wiki/server.py
|
|
14
|
+
src/mcp_server_wiki/storage.py
|
|
15
|
+
src/mcp_server_wiki/assets/tokenizer.json
|
|
16
|
+
tests/test_core.py
|