The reference parser, validator, renderer, and CLI for GEML (General Expressive Markup Language) — one format, two readers. People and AI agents co-write the same document: plain text that stays legible for people, and addressable, verifiable, and versioned for machines.
Every kind of structured content — code, tables, diagrams, math, callouts, metadata — rides on one primitive, the typed block:
=== code {#hello lang=python}
print("hi")
===
#id; geml get / geml set '#id'
read or patch one section without re-emitting the whole file (on this repo’s
own spec, ~31× less context than shipping the whole document).#id is an
error, not a silent dead link), and the parser emits a document-model JSON
with a diagnostics array, so agents and CI get a structured pass/fail signal.geml history and geml revert snapshot and rewind
revisions over a plain-text .gemlhistory sidecar.Try the format in the playground — no install. Full pitch, spec, and format comparison live in the repository.
npm install -g @geml/geml # global CLI — installs the `geml` command
# or, per project:
npm install @geml/geml # library + local bin
Requires Node ≥ 22.
The CLI is built around one question: can a single agent author and maintain an
entire .geml file from the command line — create, add, edit, delete, and copy
blocks in from other files? Three tests keep the command set honest:
#id and
the content adopts it, every write is guarded, a file is edited in place while
- streams to stdout.The command set borrows from two settled models rather than inventing one, and
they overlap where it counts. A document is a table: a block is a row, its
#id is the primary key (unique per document), and [[#id]]/[t](#id)/[^id]/
data=#id are foreign keys — so get/add/set/delete are
SELECT/INSERT/UPDATE/DELETE. A block is also a resource at a URI-like address
(file#id), named before the operation, the way REST puts the noun first. Both
models agree on a small orthogonal verb set instead of a method per use case, and
they agree on idempotence: set and delete are idempotent (deleting a missing
id is a no-op, so a retry is safe), add is not.
Where they diverge, each covers what the other cannot. The relational view names
the integrity rules: the write guard is a constraint check with rollback, and
delete merely warning about references it leaves dangling is a deferred
foreign-key check, not ON DELETE RESTRICT. It also explains rename, the verb
most open to “can this be cut” — a primary-key update with a cascading
foreign-key rewrite, irreducible because delete + add would leave every
reference dangling and nothing else rewrites references in bulk. HTTP has no
method for that at all. The REST view supplies what a database deliberately does
not: every call is stateless — no session, no current document, no config
file, no environment variable — which is what lets calls be retried,
parallelized, and piped.
Both pay off in undo. Because the verbs are orthogonal, each edit has exactly one
inverse, so revert never needs to know which verb made a change — it reconciles
a block to a revision in three cases (content changed, row missing, row extra)
and there is no fourth, while rename is its own inverse and needs no history at
all. A wider, RPC-shaped verb set (replace, move, merge, split, …) would
need a per-verb inverse and an operation log to pick one — an undo-stack engine
instead of three branches. Full rationale:
docs/design/specs/2026-07-24-geml-block-mutation-cli-design.md.
Every command reads a file path, or - for stdin. Exit codes: 0 ok ·
1 document/operation error · 2 usage error.
geml doc.geml # document-model JSON (default --to json)
geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
geml get doc.geml ['#id'] # list addressable ids, or print one block (heading id = its section)
geml set doc.geml '#id' [--head|--body] [--in F[#src]] # replace a block's content (id kept)
geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
geml rename doc.geml '#old' '#new' # rename an id + every reference to it
geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
geml history <commit|verify|show|restore|log> doc.geml [...] # .gemlhistory version sidecar
geml codemap <build|verify|render|serve|refresh|find|mcp> # your codebase's call graph as GEML docs
geml --help | --version # --version --json prints {"parser","spec"}
The agent loop: geml get a block → set/add/delete/rename it →
geml check → geml history commit — small, precise, verifiable edits.
Conversion is one entry — geml <file> [--to json|html|md|geml]; the input
format is inferred (--from overrides > extension > GEML), the target is --to
(default: GEML → JSON, Markdown → GEML), and -o names the output path.
set and add take their content from --in F (F’s block whose id equals the
target), --in F#src (F’s block #src), or stdin (raw bytes). set replaces
a whole block and normalizes the content’s id to the target — so you can fork
any block into this slot without hand-editing its id (--head swaps just the
head line, --body just the body). add inserts a fragment (one or more
blocks, or bare prose) at --append / --before #id / --after #id, keeping
the content’s own ids (a collision is refused). delete removes one or more
ids; rename rewrites an id’s declaration and every reference to it.
Mutations (set/add/delete/rename) write the whole updated document:
in place when the input is a file, or to stdout when the input is -; -o
redirects the write (-o - forces stdout), so edits pipe cleanly. Every write
is guarded — re-parsed and refused if it would break the document or drop an id
(a reference left dangling by delete is a warning, not a refusal; geml check
flags it later).
Undo is revert, which reconciles one block to a past revision (--rev, default
-1): it splices back changed content, resurrects a deleted block (placed
by its old neighbours, or --append/--before/--after), or removes a block
that did not exist then. So each forward edit has an inverse:
| forward edit | undo |
|---|---|
set #id |
revert #id (splice) |
delete #id |
revert #id (resurrect) |
add #id |
revert #id (remove) — or delete #id |
rename #old #new |
rename #new #old (self-inverse) |
revert reads the .gemlhistory sidecar, so set/delete/add undo needs a
prior geml history commit; rename is its own inverse and needs no history.
A heading’s #id addresses its whole section — the heading line through
the line before the next heading of the same-or-higher level — so the prose
under a heading is block-editable with no extra syntax.
Spans overlap: blocks nested in the section keep their own ids, and a set on
the section that drops one of them is refused by the guard. get --json on a
heading covers the same content as the raw span: a section envelope
{kind:"section", id, level, blocks:[heading, …its section's blocks]} (a
block/footnote id still prints its single model node). --head narrows
get/set/revert to ANY id’s head line — a heading’s line, or a typed
block’s opening fence line, so an agent renames a heading or edits a block’s
attributes (caption, compute, …) without touching the body. Convention: keep
the document title in === meta (title = "…"), not an H1 — a lone top-level
# section is the whole document, the telltale that it is really a title.
This package includes a standard Model Context Protocol (MCP) server that exposes GEML document CRUD operations. It runs locally and supports Windows, macOS, and Linux.
To connect it to an MCP-compatible client, provide the npx execution command and specify the --root argument (the directory containing your .geml files).
Add to your claude_desktop_config.json:
{
"mcpServers": {
"geml": {
"command": "npx",
"args": [
"-y",
"@geml/geml@latest",
"mcp",
"--root",
"/absolute/path/to/your/docs"
]
}
}
}
Run the following command to add the server:
/mcp add npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs
import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
const doc = parse(src); // { kind:"document", children, ids, diagnostics }
const ok = !doc.diagnostics.some(d => d.severity === "error");
const html = renderHtml(doc); // one self-contained HTML string
const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
const geml = mdToGeml(markdown).geml; // the inverse
const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
parse(src, { resolveDoc }) enables cross-document reference checking — pass a
function that returns another file’s source by path (or null).
Full normative spec, history-sidecar spec, and format comparison live in the
repository. The spec is itself
written in GEML (GEML-spec.geml) and parsed clean on every test run.
MIT.