Metadata-Version: 2.4
Name: local-wiki
Version: 1.0.0
Summary: Local-first agent wiki with caller-side AI conflict resolution — pending-queue commit model for safe multi-profile/multi-session writes
Author: warrior-kite
License: MIT
Keywords: wiki,mcp,knowledge-base,agent,local-first,conflict-resolution
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=1.0
Requires-Dist: PyYAML>=6.0
Requires-Dist: tokenizers>=0.19
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: build>=1.0; extra == "dev"
Dynamic: license-file

# 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...). Same code on any machine: `pip install local-wiki` or `uvx local-wiki`.

**Version 1.0.0** — production release. Runs either from PyPI (`uvx local-wiki`) or as a standalone executable (see [Executable release](#executable-release)).

## Why

- Skills should hold *experience* (how); **knowledge** (what) belongs in a wiki — this is the knowledge store for that.
- Naive file writes / single-writer tools have **no concurrency control** → multi-agent writes corrupt pages.
- This project fixes it with a **pending queue**: writes are staged (one file per draft in `pending/`), a single committer serializes them under an OS lock, and conflicts are resolved **by the caller (the agent) in its own conversation** — the server itself never calls an LLM.

## Design: AI resolution lives in the caller's conversation

The committer is intentionally **dumb and deterministic** (lock → hash check → atomic write → index update). It does **not** call any LLM:

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).
2. `wiki_commit` drains the queue under a single `LockFileEx`/`flock` lock (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.
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:
   - `wiki_resolve(id, side='merged', merged_content=...)` → apply your merge.
   - or `side='draft'` / `side='current'`.
4. Resolution removes the draft from the queue; `wiki_commit` can 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

```bash
pip install local-wiki          # or
uvx local-wiki --help           # runs the latest PyPI release
```

## Run (MCP server)

### stdio (default)

```bash
local-wiki --wiki-root C:/path/to/wiki
```

### HTTP (Streamable)

```bash
local-wiki --wiki-root C:/path/to/wiki --host 127.0.0.1 --port 8000
```

### Standalone executable (no Python required)

```bash
local-wiki.exe --wiki-root C:/path/to/wiki
```

## Wire into clients

### Hermes (`config.yaml`) — standard PyPI run, no local source build

```yaml
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 `.exe` can be pointed to directly with `command: <path>/local-wiki.exe`.

### Claude Code

```bash
claude mcp add local-wiki -- uvx local-wiki --wiki-root ~/wiki
```

## MCP Tools (12)

| Tool | Description |
|---|---|
| `wiki_write(profile, rel, content, op, session?)` | Create (direct) or stage update/delete into the pending queue |
| `wiki_commit(once?)` | Serialize 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_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_search(query, profile?)` | Search in-memory index by rel/title/keywords (case-insensitive) |
| `wiki_index(profile?, action, rel?, keywords?)` | Browse & maintain `index.json` public keywords (read tree / update / delete) |
| `wiki_create_root(key, name, workdir?, type?, owner_profile?)` | Register a new wiki root (project shard) + root_index |
| `wiki_update_root(key, name?, workdir?, owner_profile?, type?)` | Update a root's meta |
| `wiki_delete_root(key, purge?)` | Unregister a root (purge=True deletes its folder, irreversible) |

## Storage Layout (v1.0.0)

```
<wiki-root>/
├── index.json                  # root registry: {global + <project roots>} → meta
├── global/                     # cross-profile knowledge (writable)
│   ├── pages/  index.json      # public keywords per layer
├── <project-root>/             # one folder per registered project root
│   ├── pages/  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)
│   ├── merge-log.md            #   AI merge audit trail
│   └── .lock                   # OS file lock (committer holds)
```

### Layer naming — normalized to snake_case (v1.0.0)

Layer/root folder names are normalized: camelCase, hyphens and whitespace all resolve to the same snake_case key.

- `smart-park` / `smartPark` / `SmartPark` → `smart_park`
- `all_layers` dedupes legacy hyphen profiles against registered roots (`smart-park` + `smart_park` → one `smart_park`)
- `wiki_create_root` accepts 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 layer

## Data model

### Pages

Every page is Markdown with an optional YAML frontmatter; one is auto-added if missing (title = first heading, `keywords: []`).

```markdown
---
title: Docker 使用
keywords: [特有kw]
---
<正文 body>
```

- **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).
- **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 (v1.0.0 — hardened)

- **create** on a missing target → written directly (atomic), index updated.
- **update** on a missing target → rejected (`page not found`).
- **update/delete/create-on-existing** → staged into `pending/`, committed serially.
- **Target vanished after enqueue** (deleted externally between enqueue and commit):
  - `update` → commit reports `error` (never silently re-creates the page);
  - `create` → treated as a fresh create and written.
- **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.

### Concurrency

- **Cross-profile**: physical sharding (`<root>/pages/`) — different files, no collision.
- **Same-profile, 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** (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.

## Executable release

Since 1.0.0 a standalone Windows executable is built with PyInstaller (no Python/uvx needed):

```bash
# from repo root
pyinstaller --onefile --name local-wiki \
  --collect-data mcp_server_wiki \
  --collect-all mcp \
  src/mcp_server_wiki/__main__.py
# → dist/local-wiki.exe
```

The tokenizer asset (`assets/tokenizer.json`) is bundled into the executable, so offline token counting works in the exe too.

## Dev

```bash
pip install -e .[dev]
pytest                     # unit tests
python scripts/wiki_mcp_test.py            # MCP stdio integration (12 tools + boundaries)
python scripts/wiki_concurrent_stress.py   # multi-process lock stress
```

## Companion skill

`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.

## License

MIT
