WikiManager¶
File-based wiki with .md files as source of truth.
Features¶
- FTS5 full-text search
- External folder sync
- 14 content types
- Per-layer separation
Usage¶
from wiki.manager import WikiManager
fw = WikiManager(layer="user")
# Add page
fw.add_page(title="Architecture Overview", content="# Architecture\n\nTwo-layer memory system...", wiki_type="spec")
# Search
results = fw.search("architecture", limit=5)
# Count
count = fw.count()
# Reindex
fw.reindex_all()
File Structure¶
Schema lint¶
Every add() and sync_external() import runs a 6-check lint pass. Findings are logged as warnings; they never block the save.
| Check | Code | Default behavior |
|---|---|---|
| Frontmatter present | frontmatter_malformed |
warning |
Required fields (title, wiki_type) |
missing_title, missing_wiki_type |
warning |
Broken [[wikilinks]] |
broken_wikilink |
warning |
| Page length > 50,000 chars | page_too_long |
warning |
Missing INDEX.md in type dir |
missing_index |
warning |
| Tags outside vocabulary | unknown_tag |
warning |
Tag vocabulary = 7 hardcoded tags (decision, learning, todo, principle, context, spec, draft) plus the names of all currently-enabled wiki types for the layer.
Opt-in auto-fix. Only missing_index is fixable. Create the manager with auto_fix=True:
from wiki import WikiManager
wm = WikiManager(layer="user", auto_fix=True)
await wm.add("diary", "First Page", "hello", [])
# → also creates user/diary/INDEX.md stub listing all pages in the dir
The INDEX.md stub is idempotent — second add() does nothing. Other 5 checks require human judgment and stay warning-only.
To run a full layer scan outside the manager (e.g. CI):
from wiki import lint_wiki_layer
report = lint_wiki_layer("user", Path("~/.mcp-ariel-memory/wiki/user"), enabled_types)
print(report.findings)
Secret detection¶
Every add() and sync_external() import also scans content for well-known
secret formats — GitHub PATs (ghp_.../github_pat_...), API keys
(sk-.../AIza.../AKIA...), and PEM private-key headers. Matches are
logged as WARNING (never the secret value itself, only its kind) and never
block the save. The detector is conservative to avoid false positives on
short or partial strings.
from wiki.secrets import scan_secrets
findings = scan_secrets(content) # -> [SecretFinding(kind=..., location="body")]
Ref chain linking¶
Pages can be linked with typed relationships — review_of, revises, or
follows — so an agent can traverse a review/revision history. Links are
stored in a wiki_links table (separate from page content).
[[wikilinks]] in page content are auto-linked to resolvable pages on
add()/update() (resolved by title or filename stem; unresolvable links are
silently skipped). Explicit links go through the wiki_link tool or
WikiManager:
from mcp_server.tools.wiki_link import wiki_link
await wiki_link(action="add", from_path="a.md", to_path="b.md", link_type="review_of")
links = await wiki_link(action="list", from_path="a.md")
# -> {status: ok, links: [{path, link_type, direction}]}
Promotion pipeline¶
High-importance L4 rules and preferences can be promoted to wiki pages so they participate in wiki search. Deterministic, no LLM:
await wm.promote_from_core(cm, layer="user", user_id="default", min_importance=0.8)
# -> {"promoted": n, "skipped": n}
Selects core_memory facts with memory_kind in (rule, preference) at or
above the threshold. Type mapping: rule → work_notes (user) / principle_log
(agent); preference → preferences (user) / emotional_context (agent).
Idempotent — a fact whose key already has a page is skipped on re-run.
Organic operations¶
WikiManager exposes split / merge / retire for reorganizing pages without
losing history:
# split one page into several from (title, content) pairs
await wm.split("path/BigNote.md", [("Architecture", "..."), ("Data", "...")])
# merge several pages into one
await wm.merge(["path/A.md", "path/B.md"], "Merged")
# retire a page to _retired/<type>/ (removed from search, never hard-deleted)
await wm.retire("path/OldPage.md", reason="superseded")
splitleaves the source page untouched (caller deletes/retires it).mergeleaves source pages untouched, joins content under## source-titleheadings.retiremoves the file to_retired/with a> retired:marker and drops it from the index; re-running on a gone path returns an error (no crash).