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") === ``` - **Addressable** — every block can be named: an `#id`, or a content address for the ones nobody named; `geml get` / `geml set ''` read or patch one section without re-emitting the whole file (on this repo's own spec, ~**120× less context** than shipping the whole document — the block is ~590 chars whatever the document grows to). - **Verifiable** — references are checked at build time (a dangling `#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. - **Versioned** — `geml history` and `geml revert` snapshot and rewind revisions over a plain-text `.gemlhistory` sidecar. Try the format in the [playground](https://geml-spec.github.io/geml/playground/) — no install. Full pitch, spec, and format comparison live in the [repository](https://github.com/geml-spec/geml). ## Install ```sh 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: - **Complete** — every step of a document's life has a verb, so an agent never rewrites the whole file to change one block. - **Ergonomic** — few flags, sensible defaults, and pipeline-friendly I/O, so multi-step edits chain without ceremony. - **Consistent** — behavior is uniform and predictable: name a target `#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`](../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. ```sh geml doc.geml # document-model JSON (default --to json) geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML geml list doc.geml # CALL FIRST: every block, its address, kind, line range geml find "text" doc.geml|dir # search block CONTENT -> fileaddress; exit 1 = no hit geml get doc.geml [''] # list addressable blocks, or print what the selector matches geml get doc.geml '#sec' --intro # a section cuts three ways: --head | --intro | --body geml set doc.geml '' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content geml replace doc.geml OLD NEW [--within ''] # EXPERIMENTAL: literal swap, checked and reported 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 ] # validate only: diagnostics + exit code (--json for the array) # --root works on every verb above, not just check. A write is refused when the result # would not parse, so a document whose ../sibling.md links resolve only from the repo # root needs --root to be editable at all. The MCP server passes its own root for you. geml history doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one) geml codemap # your codebase's call graph as GEML docs geml mcp --root [--graph ] # serve documents (+ the code graph) over MCP geml --help | --version # --version --json prints {"parser","spec"} ``` The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it → `geml check` → `geml history save` — small, precise, verifiable edits. ### Selectors `get` and `set` take the same selector, which is a **filter over blocks**: | Selector | Matches | |---|---| | *(omitted)* | nothing — `get` **lists** every addressable block, one per line, by its shortest unique address | | `#id` | that block. A heading id addresses its **whole section** | | `'## Heading'` | a heading line copied out of the document, resolved to its id | | `'=== note'` | **every** `note` block — 0..N of them | | `'=== note@a3f9c1d2'` | one block by CONTENT, for blocks that carry no `#id` | | `'@a3f9c1d2'` | the same, with the type check dropped | `get` answers with N contents when N match (document order, count on stderr); `set` writes ONE block, so a selector matching several is refused (exit 2) with the unique address of each candidate. A section cuts three ways: `--head` is the heading line, `--intro` its opening region — everything under it up to its first subheading — and `--body` everything under it, so `--body` always contains `--intro`, and equals it when the section has no subheading. All three round-trip — `geml get f X --body | geml set f X --body` leaves the file byte-identical — and `--intro` is how a section's opening is edited without pulling its subsections into context. A block has no intro; asking for one is a usage error rather than a quiet fall back to the body. `replace` is the cheap path when the exact old text is already known and nothing needs reading — a version string in six places, a term renamed. It is the one operation where GEML can beat `sed` outright rather than imitate it: the same two short strings, but the result is re-parsed before it lands, the blocks it touched are named back to you, and it is in `.gemlhistory` to revert. It swaps a LITERAL, never a pattern, and refuses a swap that would rename an id — that is `geml rename`, which fixes the references too. **It is EXPERIMENTAL and may be withdrawn**; build nothing on it that cannot change. A write is refused when it would break the document, never merely because it removes something. A replacement that drops blocks is carried out and the dropped blocks are named on stderr — unnamed ones counted, references left dangling reported — with `geml revert` as the way back. That is the same stance `delete` takes, so removing content has one rule rather than two, and no region becomes uneditable because something inside it happens to carry an id. The round trip above drops nothing: the blocks came back in the text you sent. A `@` **content address** is the first 8 hex of the SHA-256 of the block's own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`… distinguishing byte-identical blocks. Read them out of `geml get doc.geml` — they are printed for every block that has no `#id`. Being content-derived, an address **goes stale when the block changes** and then fails with exit 1 rather than silently addressing a different block: it doubles as a precondition. That also means `set` through one prints the new address on stderr. The exact hash input is pinned in [the selector design doc](../docs/design/specs/2026-08-04-geml-get-set-selector-design-change.md) §3.4 so a second implementation computes the same values. Conversion is one entry — `geml [--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 save`; `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`: ```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: ```sh claude mcp add geml -- npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs ``` With a code graph under `--root` (`geml codemap build`), the same server also serves four read-only `geml_codemap_*` tools. Every tool and option: [`docs/mcp-guide.md`](https://github.com/geml-spec/geml/blob/main/docs/mcp-guide.md). ## Library ```js 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](https://github.com/geml-spec/geml). The spec is itself written in GEML (`GEML-spec.geml`) and parsed clean on every test run. What changed between releases: [`CHANGELOG.md`](https://github.com/geml-spec/geml/blob/main/CHANGELOG.md). The parser and the specification version independently — `geml --version --json` prints both. ## License MIT.