geml

GEML

@geml/geml

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")
===

Try the format in the playground — no install. Full pitch, spec, and format comparison live in the repository.

Install

npm install -g @geml/geml   # global CLI — installs the `geml` command
# or, per project:
npm install @geml/geml      # library + local bin

Requires Node ≥ 22.

CLI

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:

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

MCP Server

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

Claude Desktop

Add to your claude_desktop_config.json:

{
  "mcpServers": {
    "geml": {
      "command": "npx",
      "args": [
        "-y",
        "@geml/geml@latest",
        "mcp",
        "--root",
        "/absolute/path/to/your/docs"
      ]
    }
  }
}

Claude Code / CLI Clients

Run the following command to add the server:

/mcp add npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs

Library

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

Documentation

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.

License

MIT.