GEML Illustrated · the geml CLI · 中文

GEML Illustrated · CLI

The geml command does three kinds of thing: convert a document (between json, html, md and geml), read and write by block (list, find, get, set, add, delete, rename, revert), and validate (check, plus each profile's own style check, history verify, codemap verify). Core verbs never carry a profile name. Writes have a guard: a result that does not parse is refused. This page runs every verb, the address forms it accepts and the exit codes against one small document.

Board

16 rules, each with its source and status

Settled fixed by geml --help or the spec. Observed behaviour as run. GEP draft address forms that come from drafts.

Settled 12Observed 3GEP draft 1
RuleSourceStatus
General
1The file position may be - to read stdin. Write verbs (set / add / delete / rename) rewrite the file in place, or print to stdout for -; -o f redirects, -o - is stdout too. Exit 0 success, 1 a document or operation error.--helpSettled
2--root d widens cross-document resolution to directory d, and read and write verbs alike take it. A write is refused when the result fails to parse, so a document that needs the repo root to resolve ../x.md is simply uneditable without --root; otherwise the guard would read its own blind spot as breakage.--helpSettled
3Call list first: the addresses it prints are what every other verb accepts. get without a #id also lists all ids.--helpSettled
Reading
4list <file> [--json]: one line per addressable block: address, kind, line range; anonymous blocks show as === type or type@hash with a content hash; prose between blocks appears under derived addresses.--help · §4Settled
5find <pattern> [file|dir …]: searches block content and answers file<TAB>address, an address rather than a line number, so it pastes straight into get / set. A named file is searched whatever its extension (.md included); a directory walks *.geml only. No match → exit 1.--helpSettled
6get <file> [#id] [--json] [--head|--intro|--body]: a heading id = the whole section; --head the heading line only, --body everything under it, --intro up to the first sub-heading; --json gives the model node. The selector may also be a position, L27 / L27-58: the smallest block containing those lines, which turns a grep hit or a stack-trace line into an address.--helpSettled
Writing
7set <file> #id [--head|--intro|--body] [--in f[#src]|-]: replaces one block. --in F takes F's block #id, F#src takes #src, otherwise stdin verbatim; default the whole block, --head only the heading line, --body only the body.--helpSettled
8The guard: the result of a write is re-parsed and, if it fails, not written, exit 1, original untouched. Observed: feeding an unterminated code block to set yields "replacement would break the document … not written".--help · observedSettled
9add (--append | --before #id | --after #id) --in f|-: inserts a fragment, one or more blocks and prose; the content keeps its own ids, collisions are refused.--helpSettled
10delete #id [#id2…]: missing ids are skipped; a reference left dangling is only a warning, not a refusal, with a hint that check will call it an error. Observed: deleting #cmd gives one warning, writes, and check then exits 1.--help · observedSettled
11rename #old #new: changes the id and every reference, matching at id boundaries (it will not turn #cmd inside #cmdline). Observed: the block and [[#c]] become cmd together.--help · observedSettled
12replace <old> <new> [--within sel]: an EXPERIMENTAL literal replacement, checked and reported. revert <file> #id [--rev sel]: returns one block to a past version from the .gemlhistory (splice back, resurrect or delete); sel is 0 | -N | id prefix | changed, default -1.--helpSettled
Conversion and validation
13geml <file> [--to json|html|md|geml] [--from geml|md|json]: the default output is the document-model JSON; --to md is lossy (heading ids and attributes drop, with a note); --to html is self-contained, --fragment gives body markup only; --to geml is a canonical reflow; --from md reads Markdown as GEML, inferred from the extension when possible.--help · observedSettled
14check [--root d] [--json]: validation only, diagnostics plus an exit code; --json gives the diagnostics array. This is where every right-hand output in this illustrated series comes from.--helpSettled
15Profiles bring their own verbs: style check (exit 0/1/2), history save|get|restore|verify, codemap build|verify|render|serve|refresh|find; also mcp --root (11 tools, every write validated before touching disk) and skill install. Core verbs never carry a profile name.--help · profiles READMEObserved
16Address forms: #id, '## Heading' (whole section), L27-58 (position), #fy[2]["Q1"] / #meta["key"] (GEP-0011 coordinates, implemented on this branch), #form#field (GEP-0008 draft, not implemented).--help · GEP-0011 · GEP-0008GEP draft
Read/write

list → find → get → set, and one refused write

A seven-line document. Left, the document; right, the real output of each step.

Addresses come from list and paste into get and set Settled
cli.geml
=== meta
title = "cli probe"
===
# Intro {#intro}
Hello world paragraph.
=== code {#c lang=sh}
echo hi
===
## Next {#next}
See [[#c]].
set · refused by the guard
$ printf '=== code {#c lang=sh}\nunterminated\n' | geml set cli.geml '#c' --in -
error: replacement would break the document: unterminated `code` block
  (no matching === or `=== #c`) (line 6); not written
exit 1 · cli.geml untouched
shell
$ geml list cli.geml
=== meta         meta     anon  L1-3
#intro           heading  h1    L4-11  Intro
#intro-before-c  prose          L5-5      ← derived address: inside #intro, before #c
#c               code           L6-8
#next            heading  h2    L9-11  Next

$ geml find "echo" cli.geml
cli.geml	#c                          exit 0
$ geml find "nothing-here" cli.geml  exit 1

$ geml get cli.geml 'L6'                position → smallest containing block
=== code {#c lang=sh}
echo hi
===
$ geml get cli.geml '#intro' --head
# Intro {#intro}
$ geml get cli.geml '#intro' --body     everything under the heading, sub-section #next included
Hello world paragraph.
=== code {#c lang=sh}
echo hi
===
## Next {#next}
See [[#c]].
Addresses, not lines
find answers with an address because an address is still valid after the next edit and a line number is not. In the other direction, get 'L6' turns a line number into an address: the smallest block containing it.
Whole section
A heading id means the whole section: #intro is the H1 and its section runs to the end of the file, so --body includes ## Next. To change only the heading line, use --head.
The guard
set re-parses the result first and refuses to write if it broke. This is the root of block-level editing safety, and why --root matters for write verbs too: an unresolvable cross-document reference would make the guard misjudge.
Mutate

rename rewrites references, add keeps ids, delete only warns about dangling

Three writes followed by one check.

rename → add → delete → check Settled Observed
shell
$ geml rename cli.geml '#c' '#cmd'
wrote cli.geml
$ grep -n cmd cli.geml
6:=== code {#cmd lang=sh}
10:See [[#cmd]].                          ← the reference changed with it

$ printf '=== note {#n}\nadded\n===\n' | geml add cli.geml --after '#cmd' --in -
wrote cli.geml
$ geml list cli.geml | tail -4
#intro-before-cmd  prose          L5-5
#cmd               code           L6-8
#n                 note           L10-12
#next              heading  h2    L14-16  Next

$ geml delete cli.geml '#cmd'
warning: unresolved reference `#cmd` (line 12) — left dangling by delete;
  run 'geml check' to see it as an error
wrote cli.geml                            exit 0: the delete goes through
$ geml check cli.geml
error: unresolved reference `#cmd` (line 12)
1 error(s), 0 warning(s)                  exit 1
Rules
VerbStance on references
renamechanges the id and every reference, id-boundary safe
addthe fragment keeps its own ids; a collision with an existing id is refused
deletea dangling reference is a warning, not a refusal: deleting is the author's explicit intent, check makes it an error afterwards
seta result that does not parse is not written
derived addresses follow: #intro-before-c became #intro-before-cmd
Why delete does not refuse
Removing a block is the author's explicit action; the dangling reference is a consequence the tool states but does not decide for the author. The real red line is check: exit 1.
Derived addresses
A prose run's derived address is bounded by its neighbours at both ends, so renaming a neighbour changes the address. That is what §4 wants: the change is loud and never silently points at a shortened run of prose.
Convert

--to and --from

The default output is the document-model JSON; Markdown is the lossy side.

Four outputs, three inputs Settled
shell
$ geml cli.geml --to md
note: heading id/attributes dropped (Markdown has no attribute syntax)
error: unresolved reference `#cmd` (line 12)   diagnostics still reported, output still given
---
title: cli probe
---

# Intro

Hello world paragraph.

> added

## Next

$ printf '# T\n\nsome md\n' | geml - --from md --to geml
# T

some md
Formats at a glance
FlagYou getNotes
--to jsonthe document modeldefault; the shape of the §8.4 conformance surface
--to htmlself-contained HTML--fragment gives body markup only, assets via pageAssets; the right-hand labels in this series come from it
--to mdMarkdownlossy: heading ids and attributes drop, note blocks become blockquotes, data becomes code blocks; a note says so
--to gemlcanonical reflowdata's json reflowed to two-space indent
--from mdreads Markdown as GEMLinferred when the extension is .md; geml notes.md just works
--from jsonreads a --to json result backround trip
The md read layer
list / find / get read Markdown directly (--from md inferred); nothing is converted, nothing written. That is the "read long Markdown by block" usage in the geml skill.
Addresses

The ways to name one block

All verbs share the same selectors.

Selector forms Settled coordinates and field paths from drafts
SpellingNamesSource · status
#ida block with a declared id; a heading id is the whole section§4 · settled
'## Heading'by heading text, the whole section likewise--help · settled
L27 · L27-58the smallest block containing those lines--help · settled
#a-between-b · #c-before-n · #c-after-pderived addresses for prose between two blocks; list prints them§4 · settled
=== type · type@hashhow an anonymous block appears in list; type@hash pastes back into getobserved
#fy[2]["Q1"] · #fy[summary] · #meta["title"] · #cfg["tags"][1]a table row/cell/column, a summary cell, a meta value, a data value-tree nodeGEP-0011 · draft, implemented on this branch (pages 2, 3)
#vendor#contacts#emaila field inside a formGEP-0008 · draft, not implemented (page 6)
other.geml#id · other.geml#fy[2]cross-document, any of the forms above§5.2 · GEP-0011
Evidence

Probes

cli.geml was run in the session scratchpad against the current in-repo build, 1.9.2.

CommandResultMaps to
list · find (hit / miss) · get L6 · get --head / --bodyas above; find miss exits 1read/write, board 4–6
set with an unterminated blockexit 1 not written, file untouchedboard 8
rename · add --after · delete · checkreference rewritten; fragment inserted; dangling warning then check exit 1mutate, board 9–11
--to md · - --from md --to gemllossy note; stdin Markdown read as GEMLconversion
geml --help · geml history · geml codemapverbs and usage textboard 1–3, 12–15