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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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