Export & import a knowledge graph¶
Move a knowledge graph in and out of angelo in portable formats: export a zettelkasten box (or the memory research tree) to GEXF for graph tools like Gephi or networkx, export a box to an Obsidian-style markdown vault, and bulk-import a markdown vault back into the zettelkasten.
All of these ride the existing MCP dispatchers — there is no new top-level tool.
Export a zettelkasten box¶
Use the graph tool's export action (read-only — available even on a
write-free server instance):
- GEXF —
graph(action="export", graph="<box>", format="gexf")returns a GEXF 1.2 document: each note is a<node>(label = title; attributestype,tags,status,source), each link is an<edge>(label = relation, weight = confidence). Open the returnedgexffield in Gephi, or load it withnetworkx.read_gexf. - Obsidian vault —
graph(action="export", graph="<box>", format="obsidian", out_dir="<dir>")writes one.mdper note underout_dir(defaultexport/<box>-vault/). Each file has YAML frontmatter carrying the noteid(plustitle/type/tags/aliases/status) and renders every link as a Dataview-style typed wikilink, e.g.:
Conventions that make the round-trip faithful:
- The
idin frontmatter lets a re-import dedup on note identity — so re-importing an exported vault into the same box is idempotent (no duplicate notes), not just into a fresh one. - A link's
confidencerides in an optional(confidence:: <float>)subfield, a non-defaultdirectionin(direction:: <dir>), and a cross-graph target (a link into another box) in(graph:: <box>)— so all three survive export → import. - The wikilink target is a note title when that title is unique and free of
wikilink-breaking characters (
[,],|); otherwise it falls back to the target note'sid, so duplicate-title or special-character notes still resolve to exactly the right note. - A cross-graph link (one that names an entity in another box) is emitted as
a bare id with a
(graph:: <box>)marker and flagged in the export manifest'swarnings; on import the marker tells the importer not to resolve that target against local notes, so it can never silently re-point at a coincidentally same-id note in the import box.
This convention is exactly what the importer understands, so an exported vault round-trips losslessly.
Import a markdown vault¶
Use the graph tool's import action:
- Each
.mdbecomes a note: frontmatter suppliestype/tags/aliases/status; the H1 (or frontmattertitle, or filename) supplies the title; the remaining prose is the body. **wikilinks**become links. A typedrelation:: <RELATION> **target**line maps<RELATION>to a valid relation; an unknown relation is never dropped — it falls back torelatedwith a warning. A bare wikilink defaults torelated. Optional(confidence:: <float>)/(direction:: <dir>)/(graph:: <box>)subfields on a typed line restore a link's confidence/direction/cross-graph target. Prose is never silently dropped: arelation::line that is not a valid typed wikilink (e.g.relation:: see chapter 3) is kept in the body verbatim, and a typed line that carries trailing prose (contradicts **Beta** but only in bear markets) still captures thecontradictsedge (its relation is not downgraded torelated) while keeping the whole line in the body, with a warning.- Two different files in one vault that claim the same frontmatter
idare a conflict, not an idempotent re-import: the first is created and the second is skipped asduplicate-idwith a warning (its distinct content is never silently discarded). - Imports are idempotent two ways:
- By identity — a file whose frontmatter carries an
id(a vault we exported) is deduped on that id: if a note with it already exists in the target box, the import is a no-op, so re-importing into the same box creates no duplicates. The id is untrusted input — it is validated (no path separators,.., or filename-unsafe characters) before use and is only ever used to mint a new note, never to overwrite an existing one. - By content — otherwise each note is stamped with its
content_hash, so re-importing the same file is skipped. - Imported notes are marked
epistemic_status = inferred— they carry no PDF/quote grounding, unlike notes extracted from a source.
Propose-only imports¶
When the server runs under ZK_PROPOSE_ONLY (or ZK_DISABLE_WRITE), import
returns a dry-run manifest of the notes and links it would create and
writes nothing to the canonical store — review it before running a real import.
Security¶
Vault import is the one untrusted-input surface here, so every byte is treated as data:
- Paths are resolved to a realpath and confirmed to stay under
source_dir; path traversal and symlink escape are rejected. - Per-file size and total file-count caps guard against oversized files and directory bombs.
- Frontmatter is parsed with a safe YAML loader; file content is never executed, and no XML is parsed (so there is no external-entity/XXE surface).
Export the memory research tree¶
Export the persistent memory tree (entry nodes plus has_child / related
edges) to GEXF with the relate tool:
It returns a gexf field with a GEXF 1.2 document (each live entry is a node
labelled by its title, with type/status attributes) ready for Gephi or
networkx. Discarded/tombstoned entries are filtered out (never emitted as live
nodes), and the tree is auto-synced from disk before export.
Related¶
Semantically related entries from the memory graph.