@geml/geml 1.8.2 → 1.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +288 -288
- package/codemap/adapters/crg.mjs +120 -120
- package/codemap/adapters/joern.mjs +131 -131
- package/codemap/adapters/scip.mjs +658 -658
- package/codemap/browser-stub.mjs +34 -34
- package/codemap/build.mjs +629 -629
- package/codemap/cross-stack.mjs +303 -303
- package/codemap/detect.mjs +399 -399
- package/codemap/emit.mjs +510 -510
- package/codemap/entries.mjs +129 -129
- package/codemap/exclude.mjs +56 -56
- package/codemap/find.mjs +49 -49
- package/codemap/foldings.mjs +110 -110
- package/codemap/joern-export.sc +83 -83
- package/codemap/mcp-server.mjs +434 -434
- package/codemap/normalize.mjs +275 -275
- package/codemap/recipe-trust.mjs +103 -103
- package/codemap/refresh.mjs +313 -313
- package/codemap/render-all.mjs +90 -90
- package/codemap/serve.mjs +585 -585
- package/codemap/sfc-virtualize.mjs +367 -367
- package/codemap/verify.mjs +158 -158
- package/dist/cli.js +129 -129
- package/dist/geml.js +73 -25
- package/dist/mcp.js +19 -19
- package/dist/render-html.js +35 -35
- package/dist/render.js +157 -157
- package/dist/serialize.js +7 -1
- package/dist/to-md.js +8 -1
- package/package.json +67 -67
- package/skill/SKILL.md +167 -167
- package/skill/references/authoring.geml +369 -369
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 GEML contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GEML contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,288 +1,288 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<picture>
|
|
3
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-dark.svg">
|
|
4
|
-
<img src="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-light.svg" alt="GEML" width="300">
|
|
5
|
-
</picture>
|
|
6
|
-
</p>
|
|
7
|
-
|
|
8
|
-
# @geml/geml
|
|
9
|
-
|
|
10
|
-
The reference parser, validator, renderer, and CLI for **GEML** (General
|
|
11
|
-
Expressive Markup Language) — **one format, two readers.** People and AI agents
|
|
12
|
-
co-write the same document: plain text that stays legible for people, and
|
|
13
|
-
**addressable, verifiable, and versioned** for machines.
|
|
14
|
-
|
|
15
|
-
Every kind of structured content — code, tables, diagrams, math, callouts,
|
|
16
|
-
metadata — rides on **one** primitive, the typed block:
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
=== code {#hello lang=python}
|
|
20
|
-
print("hi")
|
|
21
|
-
===
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
- **Addressable** — every block can be named: an `#id`, or a content address for
|
|
25
|
-
the ones nobody named; `geml get` / `geml set '<selector>'`
|
|
26
|
-
read or patch one section without re-emitting the whole file (on this repo's
|
|
27
|
-
own spec, ~**120× less context** than shipping the whole document — the block
|
|
28
|
-
is ~590 chars whatever the document grows to).
|
|
29
|
-
- **Verifiable** — references are checked at build time (a dangling `#id` is an
|
|
30
|
-
error, not a silent dead link), and the parser emits a document-model JSON
|
|
31
|
-
with a `diagnostics` array, so agents and CI get a structured pass/fail signal.
|
|
32
|
-
- **Versioned** — `geml history` and `geml revert` snapshot and rewind
|
|
33
|
-
revisions over a plain-text `.gemlhistory` sidecar.
|
|
34
|
-
|
|
35
|
-
Try the format in the [playground](https://geml-spec.github.io/geml/playground/)
|
|
36
|
-
— no install. Full pitch, spec, and format comparison live in the
|
|
37
|
-
[repository](https://github.com/geml-spec/geml).
|
|
38
|
-
|
|
39
|
-
## Install
|
|
40
|
-
|
|
41
|
-
```sh
|
|
42
|
-
npm install -g @geml/geml # global CLI — installs the `geml` command
|
|
43
|
-
# or, per project:
|
|
44
|
-
npm install @geml/geml # library + local bin
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Requires Node ≥ 22.
|
|
48
|
-
|
|
49
|
-
## CLI
|
|
50
|
-
|
|
51
|
-
The CLI is built around one question: can a single agent author and maintain an
|
|
52
|
-
entire `.geml` file from the command line — create, add, edit, delete, and copy
|
|
53
|
-
blocks in from other files? Three tests keep the command set honest:
|
|
54
|
-
|
|
55
|
-
- **Complete** — every step of a document's life has a verb, so an agent never
|
|
56
|
-
rewrites the whole file to change one block.
|
|
57
|
-
- **Ergonomic** — few flags, sensible defaults, and pipeline-friendly I/O, so
|
|
58
|
-
multi-step edits chain without ceremony.
|
|
59
|
-
- **Consistent** — behavior is uniform and predictable: name a target `#id` and
|
|
60
|
-
the content adopts it, every write is guarded, a file is edited in place while
|
|
61
|
-
`-` streams to stdout.
|
|
62
|
-
|
|
63
|
-
The command set borrows from two settled models rather than inventing one, and
|
|
64
|
-
they overlap where it counts. **A document is a table**: a block is a row, its
|
|
65
|
-
`#id` is the primary key (unique per document), and `[[#id]]`/`[t](#id)`/`[^id]`/
|
|
66
|
-
`data=#id` are foreign keys — so `get`/`add`/`set`/`delete` are
|
|
67
|
-
SELECT/INSERT/UPDATE/DELETE. **A block is also a resource** at a URI-like address
|
|
68
|
-
(`file#id`), named before the operation, the way REST puts the noun first. Both
|
|
69
|
-
models agree on a small orthogonal verb set instead of a method per use case, and
|
|
70
|
-
they agree on idempotence: `set` and `delete` are idempotent (deleting a missing
|
|
71
|
-
id is a no-op, so a retry is safe), `add` is not.
|
|
72
|
-
|
|
73
|
-
Where they diverge, each covers what the other cannot. The relational view names
|
|
74
|
-
the integrity rules: the write guard is a constraint check with rollback, and
|
|
75
|
-
`delete` merely *warning* about references it leaves dangling is a **deferred**
|
|
76
|
-
foreign-key check, not `ON DELETE RESTRICT`. It also explains `rename`, the verb
|
|
77
|
-
most open to "can this be cut" — a **primary-key update with a cascading
|
|
78
|
-
foreign-key rewrite**, irreducible because `delete` + `add` would leave every
|
|
79
|
-
reference dangling and nothing else rewrites references in bulk. HTTP has no
|
|
80
|
-
method for that at all. The REST view supplies what a database deliberately does
|
|
81
|
-
not: every call is **stateless** — no session, no current document, no config
|
|
82
|
-
file, no environment variable — which is what lets calls be retried,
|
|
83
|
-
parallelized, and piped.
|
|
84
|
-
|
|
85
|
-
Both pay off in undo. Because the verbs are orthogonal, each edit has exactly one
|
|
86
|
-
inverse, so `revert` never needs to know which verb made a change — it reconciles
|
|
87
|
-
a block to a revision in three cases (content changed, row missing, row extra)
|
|
88
|
-
and there is no fourth, while `rename` is its own inverse and needs no history at
|
|
89
|
-
all. A wider, RPC-shaped verb set (`replace`, `move`, `merge`, `split`, …) would
|
|
90
|
-
need a per-verb inverse and an operation log to pick one — an undo-stack engine
|
|
91
|
-
instead of three branches. Full rationale:
|
|
92
|
-
[`docs/design/specs/2026-07-24-geml-block-mutation-cli-design.md`](../docs/design/specs/2026-07-24-geml-block-mutation-cli-design.md).
|
|
93
|
-
|
|
94
|
-
Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
|
|
95
|
-
`1` document/operation error · `2` usage error.
|
|
96
|
-
|
|
97
|
-
```sh
|
|
98
|
-
geml doc.geml # document-model JSON (default --to json)
|
|
99
|
-
geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
|
|
100
|
-
geml list doc.geml # CALL FIRST: every block, its address, kind, line range
|
|
101
|
-
geml find "text" doc.geml|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit
|
|
102
|
-
geml get doc.geml ['<selector>'] # list addressable blocks, or print what the selector matches
|
|
103
|
-
geml get doc.geml '#sec' --intro # a section cuts three ways: --head | --intro | --body
|
|
104
|
-
geml set doc.geml '<selector>' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content
|
|
105
|
-
geml replace doc.geml OLD NEW [--within '<selector>'] # EXPERIMENTAL: literal swap, checked and reported
|
|
106
|
-
geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
|
|
107
|
-
geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
|
|
108
|
-
geml rename doc.geml '#old' '#new' # rename an id + every reference to it
|
|
109
|
-
geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
|
|
110
|
-
geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
|
|
111
|
-
# --root works on every verb above, not just check. A write is refused when the result
|
|
112
|
-
# would not parse, so a document whose ../sibling.md links resolve only from the repo
|
|
113
|
-
# root needs --root to be editable at all. The MCP server passes its own root for you.
|
|
114
|
-
geml history <save|get|restore|verify> doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one)
|
|
115
|
-
geml codemap <build|verify|render|serve|refresh|find> # your codebase's call graph as GEML docs
|
|
116
|
-
geml mcp --root <dir> [--graph <dir>] # serve documents (+ the code graph) over MCP
|
|
117
|
-
geml --help | --version # --version --json prints {"parser","spec"}
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it →
|
|
121
|
-
`geml check` → `geml history save` — small, precise, verifiable edits.
|
|
122
|
-
|
|
123
|
-
### Selectors
|
|
124
|
-
|
|
125
|
-
`get` and `set` take the same selector, which is a **filter over blocks**:
|
|
126
|
-
|
|
127
|
-
| Selector | Matches |
|
|
128
|
-
|---|---|
|
|
129
|
-
| *(omitted)* | nothing — `get` **lists** every addressable block, one per line, by its shortest unique address |
|
|
130
|
-
| `#id` | that block. A heading id addresses its **whole section** |
|
|
131
|
-
| `'## Heading'` | a heading line copied out of the document, resolved to its id |
|
|
132
|
-
| `'=== note'` | **every** `note` block — 0..N of them |
|
|
133
|
-
| `'=== note@a3f9c1d2'` | one block by CONTENT, for blocks that carry no `#id` |
|
|
134
|
-
| `'@a3f9c1d2'` | the same, with the type check dropped |
|
|
135
|
-
|
|
136
|
-
`get` answers with N contents when N match (document order, count on stderr);
|
|
137
|
-
`set` writes ONE block, so a selector matching several is refused (exit 2) with
|
|
138
|
-
the unique address of each candidate. A section cuts three ways: `--head` is the
|
|
139
|
-
heading line, `--intro` its opening region — everything under it up to its first
|
|
140
|
-
subheading — and `--body` everything under it, so `--body` always contains
|
|
141
|
-
`--intro`, and equals it when the section has no subheading. All three
|
|
142
|
-
round-trip — `geml get f X --body | geml set f X --body` leaves the file
|
|
143
|
-
byte-identical — and `--intro` is how a section's opening is edited without
|
|
144
|
-
pulling its subsections into context. A block has no intro; asking for one is a
|
|
145
|
-
usage error rather than a quiet fall back to the body.
|
|
146
|
-
|
|
147
|
-
`replace` is the cheap path when the exact old text is already known and nothing
|
|
148
|
-
needs reading — a version string in six places, a term renamed. It is the one
|
|
149
|
-
operation where GEML can beat `sed` outright rather than imitate it: the same
|
|
150
|
-
two short strings, but the result is re-parsed before it lands, the blocks it
|
|
151
|
-
touched are named back to you, and it is in `.gemlhistory` to revert. It swaps a
|
|
152
|
-
LITERAL, never a pattern, and refuses a swap that would rename an id — that is
|
|
153
|
-
`geml rename`, which fixes the references too. **It is EXPERIMENTAL and may be
|
|
154
|
-
withdrawn**; build nothing on it that cannot change.
|
|
155
|
-
|
|
156
|
-
A write is refused when it would break the document, never merely because it
|
|
157
|
-
removes something. A replacement that drops blocks is carried out and the
|
|
158
|
-
dropped blocks are named on stderr — unnamed ones counted, references left
|
|
159
|
-
dangling reported — with `geml revert` as the way back. That is the same stance
|
|
160
|
-
`delete` takes, so removing content has one rule rather than two, and no region
|
|
161
|
-
becomes uneditable because something inside it happens to carry an id. The
|
|
162
|
-
round trip above drops nothing: the blocks came back in the text you sent.
|
|
163
|
-
|
|
164
|
-
A `@<hex>` **content address** is the first 8 hex of the SHA-256 of the block's
|
|
165
|
-
own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`…
|
|
166
|
-
distinguishing byte-identical blocks. Read them out of `geml get doc.geml` —
|
|
167
|
-
they are printed for every block that has no `#id`. Being content-derived, an
|
|
168
|
-
address **goes stale when the block changes** and then fails with exit 1 rather
|
|
169
|
-
than silently addressing a different block: it doubles as a precondition. That
|
|
170
|
-
also means `set` through one prints the new address on stderr. The exact hash
|
|
171
|
-
input is pinned in
|
|
172
|
-
[the selector design doc](../docs/design/specs/2026-08-04-geml-get-set-selector-design-change.md)
|
|
173
|
-
§3.4 so a second implementation computes the same values.
|
|
174
|
-
|
|
175
|
-
Conversion is one entry — `geml <file> [--to json|html|md|geml]`; the input
|
|
176
|
-
format is inferred (`--from` overrides > extension > GEML), the target is `--to`
|
|
177
|
-
(default: GEML → JSON, Markdown → GEML), and `-o` names the output path.
|
|
178
|
-
|
|
179
|
-
`set` and `add` take their content from `--in F` (F's block whose id equals the
|
|
180
|
-
target), `--in F#src` (F's block `#src`), or stdin (raw bytes). `set` **replaces
|
|
181
|
-
a whole block** and normalizes the content's id to the target — so you can fork
|
|
182
|
-
any block into this slot without hand-editing its id (`--head` swaps just the
|
|
183
|
-
head line, `--body` just the body). `add` **inserts a fragment** (one or more
|
|
184
|
-
blocks, or bare prose) at `--append` / `--before #id` / `--after #id`, keeping
|
|
185
|
-
the content's own ids (a collision is refused). `delete` removes one or more
|
|
186
|
-
ids; `rename` rewrites an id's declaration and every reference to it.
|
|
187
|
-
|
|
188
|
-
Mutations (`set`/`add`/`delete`/`rename`) write the **whole updated document**:
|
|
189
|
-
in place when the input is a file, or to **stdout** when the input is `-`; `-o`
|
|
190
|
-
redirects the write (`-o -` forces stdout), so edits pipe cleanly. Every write
|
|
191
|
-
is guarded — re-parsed and refused if it would break the document or drop an id
|
|
192
|
-
(a reference left dangling by `delete` is a warning, not a refusal; `geml check`
|
|
193
|
-
flags it later).
|
|
194
|
-
|
|
195
|
-
Undo is `revert`, which reconciles one block to a past revision (`--rev`, default
|
|
196
|
-
`-1`): it **splices** back changed content, **resurrects** a deleted block (placed
|
|
197
|
-
by its old neighbours, or `--append`/`--before`/`--after`), or **removes** a block
|
|
198
|
-
that did not exist then. So each forward edit has an inverse:
|
|
199
|
-
|
|
200
|
-
| forward edit | undo |
|
|
201
|
-
|---|---|
|
|
202
|
-
| `set #id` | `revert #id` (splice) |
|
|
203
|
-
| `delete #id` | `revert #id` (resurrect) |
|
|
204
|
-
| `add #id` | `revert #id` (remove) — or `delete #id` |
|
|
205
|
-
| `rename #old #new` | `rename #new #old` (self-inverse) |
|
|
206
|
-
|
|
207
|
-
`revert` reads the `.gemlhistory` sidecar, so `set`/`delete`/`add` undo needs a
|
|
208
|
-
prior `geml history save`; `rename` is its own inverse and needs no history.
|
|
209
|
-
|
|
210
|
-
A **heading's** `#id` addresses its whole **section** — the heading line through
|
|
211
|
-
the line before the next heading of the same-or-higher level — so the prose
|
|
212
|
-
under a heading is block-editable with no extra syntax.
|
|
213
|
-
Spans overlap: blocks nested in the section keep their own ids, and a `set` on
|
|
214
|
-
the section that drops one of them is refused by the guard. `get --json` on a
|
|
215
|
-
heading covers the same content as the raw span: a section envelope
|
|
216
|
-
`{kind:"section", id, level, blocks:[heading, …its section's blocks]}` (a
|
|
217
|
-
block/footnote id still prints its single model node). `--head` narrows
|
|
218
|
-
`get`/`set`/`revert` to ANY id's head line — a heading's line, or a typed
|
|
219
|
-
block's opening fence line, so an agent renames a heading or edits a block's
|
|
220
|
-
attributes (caption, compute, …) without touching the body. Convention: keep
|
|
221
|
-
the document title in `=== meta` (`title = "…"`), not an H1 — a lone top-level
|
|
222
|
-
`#` section is the whole document, the telltale that it is really a title.
|
|
223
|
-
|
|
224
|
-
## MCP Server
|
|
225
|
-
|
|
226
|
-
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.
|
|
227
|
-
|
|
228
|
-
To connect it to an MCP-compatible client, provide the `npx` execution command and specify the `--root` argument (the directory containing your `.geml` files).
|
|
229
|
-
|
|
230
|
-
### Claude Desktop
|
|
231
|
-
Add to your `claude_desktop_config.json`:
|
|
232
|
-
```json
|
|
233
|
-
{
|
|
234
|
-
"mcpServers": {
|
|
235
|
-
"geml": {
|
|
236
|
-
"command": "npx",
|
|
237
|
-
"args": [
|
|
238
|
-
"-y",
|
|
239
|
-
"@geml/geml@latest",
|
|
240
|
-
"mcp",
|
|
241
|
-
"--root",
|
|
242
|
-
"/absolute/path/to/your/docs"
|
|
243
|
-
]
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
### Claude Code / CLI Clients
|
|
250
|
-
Run the following command to add the server:
|
|
251
|
-
```sh
|
|
252
|
-
claude mcp add geml -- npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
With a code graph under `--root` (`geml codemap build`), the same server also
|
|
256
|
-
serves four read-only `geml_codemap_*` tools. Every tool and option:
|
|
257
|
-
[`docs/mcp-guide.md`](https://github.com/geml-spec/geml/blob/main/docs/mcp-guide.md).
|
|
258
|
-
|
|
259
|
-
## Library
|
|
260
|
-
|
|
261
|
-
```js
|
|
262
|
-
import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
|
|
263
|
-
|
|
264
|
-
const doc = parse(src); // { kind:"document", children, ids, diagnostics }
|
|
265
|
-
const ok = !doc.diagnostics.some(d => d.severity === "error");
|
|
266
|
-
const html = renderHtml(doc); // one self-contained HTML string
|
|
267
|
-
const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
|
|
268
|
-
const geml = mdToGeml(markdown).geml; // the inverse
|
|
269
|
-
const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
`parse(src, { resolveDoc })` enables cross-document reference checking — pass a
|
|
273
|
-
function that returns another file's source by path (or `null`).
|
|
274
|
-
|
|
275
|
-
## Documentation
|
|
276
|
-
|
|
277
|
-
Full normative spec, history-sidecar spec, and format comparison live in the
|
|
278
|
-
[repository](https://github.com/geml-spec/geml). The spec is itself
|
|
279
|
-
written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
|
|
280
|
-
|
|
281
|
-
What changed between releases:
|
|
282
|
-
[`CHANGELOG.md`](https://github.com/geml-spec/geml/blob/main/CHANGELOG.md).
|
|
283
|
-
The parser and the specification version independently — `geml --version --json`
|
|
284
|
-
prints both.
|
|
285
|
-
|
|
286
|
-
## License
|
|
287
|
-
|
|
288
|
-
MIT.
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-dark.svg">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/geml-spec/geml/main/docs/assets/logo/geml-logo-light.svg" alt="GEML" width="300">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
# @geml/geml
|
|
9
|
+
|
|
10
|
+
The reference parser, validator, renderer, and CLI for **GEML** (General
|
|
11
|
+
Expressive Markup Language) — **one format, two readers.** People and AI agents
|
|
12
|
+
co-write the same document: plain text that stays legible for people, and
|
|
13
|
+
**addressable, verifiable, and versioned** for machines.
|
|
14
|
+
|
|
15
|
+
Every kind of structured content — code, tables, diagrams, math, callouts,
|
|
16
|
+
metadata — rides on **one** primitive, the typed block:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
=== code {#hello lang=python}
|
|
20
|
+
print("hi")
|
|
21
|
+
===
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Addressable** — every block can be named: an `#id`, or a content address for
|
|
25
|
+
the ones nobody named; `geml get` / `geml set '<selector>'`
|
|
26
|
+
read or patch one section without re-emitting the whole file (on this repo's
|
|
27
|
+
own spec, ~**120× less context** than shipping the whole document — the block
|
|
28
|
+
is ~590 chars whatever the document grows to).
|
|
29
|
+
- **Verifiable** — references are checked at build time (a dangling `#id` is an
|
|
30
|
+
error, not a silent dead link), and the parser emits a document-model JSON
|
|
31
|
+
with a `diagnostics` array, so agents and CI get a structured pass/fail signal.
|
|
32
|
+
- **Versioned** — `geml history` and `geml revert` snapshot and rewind
|
|
33
|
+
revisions over a plain-text `.gemlhistory` sidecar.
|
|
34
|
+
|
|
35
|
+
Try the format in the [playground](https://geml-spec.github.io/geml/playground/)
|
|
36
|
+
— no install. Full pitch, spec, and format comparison live in the
|
|
37
|
+
[repository](https://github.com/geml-spec/geml).
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
npm install -g @geml/geml # global CLI — installs the `geml` command
|
|
43
|
+
# or, per project:
|
|
44
|
+
npm install @geml/geml # library + local bin
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Requires Node ≥ 22.
|
|
48
|
+
|
|
49
|
+
## CLI
|
|
50
|
+
|
|
51
|
+
The CLI is built around one question: can a single agent author and maintain an
|
|
52
|
+
entire `.geml` file from the command line — create, add, edit, delete, and copy
|
|
53
|
+
blocks in from other files? Three tests keep the command set honest:
|
|
54
|
+
|
|
55
|
+
- **Complete** — every step of a document's life has a verb, so an agent never
|
|
56
|
+
rewrites the whole file to change one block.
|
|
57
|
+
- **Ergonomic** — few flags, sensible defaults, and pipeline-friendly I/O, so
|
|
58
|
+
multi-step edits chain without ceremony.
|
|
59
|
+
- **Consistent** — behavior is uniform and predictable: name a target `#id` and
|
|
60
|
+
the content adopts it, every write is guarded, a file is edited in place while
|
|
61
|
+
`-` streams to stdout.
|
|
62
|
+
|
|
63
|
+
The command set borrows from two settled models rather than inventing one, and
|
|
64
|
+
they overlap where it counts. **A document is a table**: a block is a row, its
|
|
65
|
+
`#id` is the primary key (unique per document), and `[[#id]]`/`[t](#id)`/`[^id]`/
|
|
66
|
+
`data=#id` are foreign keys — so `get`/`add`/`set`/`delete` are
|
|
67
|
+
SELECT/INSERT/UPDATE/DELETE. **A block is also a resource** at a URI-like address
|
|
68
|
+
(`file#id`), named before the operation, the way REST puts the noun first. Both
|
|
69
|
+
models agree on a small orthogonal verb set instead of a method per use case, and
|
|
70
|
+
they agree on idempotence: `set` and `delete` are idempotent (deleting a missing
|
|
71
|
+
id is a no-op, so a retry is safe), `add` is not.
|
|
72
|
+
|
|
73
|
+
Where they diverge, each covers what the other cannot. The relational view names
|
|
74
|
+
the integrity rules: the write guard is a constraint check with rollback, and
|
|
75
|
+
`delete` merely *warning* about references it leaves dangling is a **deferred**
|
|
76
|
+
foreign-key check, not `ON DELETE RESTRICT`. It also explains `rename`, the verb
|
|
77
|
+
most open to "can this be cut" — a **primary-key update with a cascading
|
|
78
|
+
foreign-key rewrite**, irreducible because `delete` + `add` would leave every
|
|
79
|
+
reference dangling and nothing else rewrites references in bulk. HTTP has no
|
|
80
|
+
method for that at all. The REST view supplies what a database deliberately does
|
|
81
|
+
not: every call is **stateless** — no session, no current document, no config
|
|
82
|
+
file, no environment variable — which is what lets calls be retried,
|
|
83
|
+
parallelized, and piped.
|
|
84
|
+
|
|
85
|
+
Both pay off in undo. Because the verbs are orthogonal, each edit has exactly one
|
|
86
|
+
inverse, so `revert` never needs to know which verb made a change — it reconciles
|
|
87
|
+
a block to a revision in three cases (content changed, row missing, row extra)
|
|
88
|
+
and there is no fourth, while `rename` is its own inverse and needs no history at
|
|
89
|
+
all. A wider, RPC-shaped verb set (`replace`, `move`, `merge`, `split`, …) would
|
|
90
|
+
need a per-verb inverse and an operation log to pick one — an undo-stack engine
|
|
91
|
+
instead of three branches. Full rationale:
|
|
92
|
+
[`docs/design/specs/2026-07-24-geml-block-mutation-cli-design.md`](../docs/design/specs/2026-07-24-geml-block-mutation-cli-design.md).
|
|
93
|
+
|
|
94
|
+
Every command reads a file path, or `-` for stdin. Exit codes: `0` ok ·
|
|
95
|
+
`1` document/operation error · `2` usage error.
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
geml doc.geml # document-model JSON (default --to json)
|
|
99
|
+
geml doc.geml --to md|html|geml # convert; geml notes.md -> GEML
|
|
100
|
+
geml list doc.geml # CALL FIRST: every block, its address, kind, line range
|
|
101
|
+
geml find "text" doc.geml|dir # search block CONTENT -> file<TAB>address; exit 1 = no hit
|
|
102
|
+
geml get doc.geml ['<selector>'] # list addressable blocks, or print what the selector matches
|
|
103
|
+
geml get doc.geml '#sec' --intro # a section cuts three ways: --head | --intro | --body
|
|
104
|
+
geml set doc.geml '<selector>' [--head|--intro|--body] [--in F[#src]] # replace ONE block's content
|
|
105
|
+
geml replace doc.geml OLD NEW [--within '<selector>'] # EXPERIMENTAL: literal swap, checked and reported
|
|
106
|
+
geml add doc.geml (--append|--before #id|--after #id) [--in F[#src]] # insert a fragment
|
|
107
|
+
geml delete doc.geml '#id' ['#id2' …] # remove one or more blocks
|
|
108
|
+
geml rename doc.geml '#old' '#new' # rename an id + every reference to it
|
|
109
|
+
geml revert doc.geml '#id' [--rev -1] # undo a block: splice / resurrect / remove
|
|
110
|
+
geml check doc.geml [--root <dir>] # validate only: diagnostics + exit code (--json for the array)
|
|
111
|
+
# --root works on every verb above, not just check. A write is refused when the result
|
|
112
|
+
# would not parse, so a document whose ../sibling.md links resolve only from the repo
|
|
113
|
+
# root needs --root to be editable at all. The MCP server passes its own root for you.
|
|
114
|
+
geml history <save|get|restore|verify> doc.geml [...] # .gemlhistory version sidecar (get = list revisions, or print one)
|
|
115
|
+
geml codemap <build|verify|render|serve|refresh|find> # your codebase's call graph as GEML docs
|
|
116
|
+
geml mcp --root <dir> [--graph <dir>] # serve documents (+ the code graph) over MCP
|
|
117
|
+
geml --help | --version # --version --json prints {"parser","spec"}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The agent loop: `geml get` a block → `set`/`add`/`delete`/`rename` it →
|
|
121
|
+
`geml check` → `geml history save` — small, precise, verifiable edits.
|
|
122
|
+
|
|
123
|
+
### Selectors
|
|
124
|
+
|
|
125
|
+
`get` and `set` take the same selector, which is a **filter over blocks**:
|
|
126
|
+
|
|
127
|
+
| Selector | Matches |
|
|
128
|
+
|---|---|
|
|
129
|
+
| *(omitted)* | nothing — `get` **lists** every addressable block, one per line, by its shortest unique address |
|
|
130
|
+
| `#id` | that block. A heading id addresses its **whole section** |
|
|
131
|
+
| `'## Heading'` | a heading line copied out of the document, resolved to its id |
|
|
132
|
+
| `'=== note'` | **every** `note` block — 0..N of them |
|
|
133
|
+
| `'=== note@a3f9c1d2'` | one block by CONTENT, for blocks that carry no `#id` |
|
|
134
|
+
| `'@a3f9c1d2'` | the same, with the type check dropped |
|
|
135
|
+
|
|
136
|
+
`get` answers with N contents when N match (document order, count on stderr);
|
|
137
|
+
`set` writes ONE block, so a selector matching several is refused (exit 2) with
|
|
138
|
+
the unique address of each candidate. A section cuts three ways: `--head` is the
|
|
139
|
+
heading line, `--intro` its opening region — everything under it up to its first
|
|
140
|
+
subheading — and `--body` everything under it, so `--body` always contains
|
|
141
|
+
`--intro`, and equals it when the section has no subheading. All three
|
|
142
|
+
round-trip — `geml get f X --body | geml set f X --body` leaves the file
|
|
143
|
+
byte-identical — and `--intro` is how a section's opening is edited without
|
|
144
|
+
pulling its subsections into context. A block has no intro; asking for one is a
|
|
145
|
+
usage error rather than a quiet fall back to the body.
|
|
146
|
+
|
|
147
|
+
`replace` is the cheap path when the exact old text is already known and nothing
|
|
148
|
+
needs reading — a version string in six places, a term renamed. It is the one
|
|
149
|
+
operation where GEML can beat `sed` outright rather than imitate it: the same
|
|
150
|
+
two short strings, but the result is re-parsed before it lands, the blocks it
|
|
151
|
+
touched are named back to you, and it is in `.gemlhistory` to revert. It swaps a
|
|
152
|
+
LITERAL, never a pattern, and refuses a swap that would rename an id — that is
|
|
153
|
+
`geml rename`, which fixes the references too. **It is EXPERIMENTAL and may be
|
|
154
|
+
withdrawn**; build nothing on it that cannot change.
|
|
155
|
+
|
|
156
|
+
A write is refused when it would break the document, never merely because it
|
|
157
|
+
removes something. A replacement that drops blocks is carried out and the
|
|
158
|
+
dropped blocks are named on stderr — unnamed ones counted, references left
|
|
159
|
+
dangling reported — with `geml revert` as the way back. That is the same stance
|
|
160
|
+
`delete` takes, so removing content has one rule rather than two, and no region
|
|
161
|
+
becomes uneditable because something inside it happens to carry an id. The
|
|
162
|
+
round trip above drops nothing: the blocks came back in the text you sent.
|
|
163
|
+
|
|
164
|
+
A `@<hex>` **content address** is the first 8 hex of the SHA-256 of the block's
|
|
165
|
+
own text (line endings normalized to LF, no trailing newline), with `~1`, `~2`…
|
|
166
|
+
distinguishing byte-identical blocks. Read them out of `geml get doc.geml` —
|
|
167
|
+
they are printed for every block that has no `#id`. Being content-derived, an
|
|
168
|
+
address **goes stale when the block changes** and then fails with exit 1 rather
|
|
169
|
+
than silently addressing a different block: it doubles as a precondition. That
|
|
170
|
+
also means `set` through one prints the new address on stderr. The exact hash
|
|
171
|
+
input is pinned in
|
|
172
|
+
[the selector design doc](../docs/design/specs/2026-08-04-geml-get-set-selector-design-change.md)
|
|
173
|
+
§3.4 so a second implementation computes the same values.
|
|
174
|
+
|
|
175
|
+
Conversion is one entry — `geml <file> [--to json|html|md|geml]`; the input
|
|
176
|
+
format is inferred (`--from` overrides > extension > GEML), the target is `--to`
|
|
177
|
+
(default: GEML → JSON, Markdown → GEML), and `-o` names the output path.
|
|
178
|
+
|
|
179
|
+
`set` and `add` take their content from `--in F` (F's block whose id equals the
|
|
180
|
+
target), `--in F#src` (F's block `#src`), or stdin (raw bytes). `set` **replaces
|
|
181
|
+
a whole block** and normalizes the content's id to the target — so you can fork
|
|
182
|
+
any block into this slot without hand-editing its id (`--head` swaps just the
|
|
183
|
+
head line, `--body` just the body). `add` **inserts a fragment** (one or more
|
|
184
|
+
blocks, or bare prose) at `--append` / `--before #id` / `--after #id`, keeping
|
|
185
|
+
the content's own ids (a collision is refused). `delete` removes one or more
|
|
186
|
+
ids; `rename` rewrites an id's declaration and every reference to it.
|
|
187
|
+
|
|
188
|
+
Mutations (`set`/`add`/`delete`/`rename`) write the **whole updated document**:
|
|
189
|
+
in place when the input is a file, or to **stdout** when the input is `-`; `-o`
|
|
190
|
+
redirects the write (`-o -` forces stdout), so edits pipe cleanly. Every write
|
|
191
|
+
is guarded — re-parsed and refused if it would break the document or drop an id
|
|
192
|
+
(a reference left dangling by `delete` is a warning, not a refusal; `geml check`
|
|
193
|
+
flags it later).
|
|
194
|
+
|
|
195
|
+
Undo is `revert`, which reconciles one block to a past revision (`--rev`, default
|
|
196
|
+
`-1`): it **splices** back changed content, **resurrects** a deleted block (placed
|
|
197
|
+
by its old neighbours, or `--append`/`--before`/`--after`), or **removes** a block
|
|
198
|
+
that did not exist then. So each forward edit has an inverse:
|
|
199
|
+
|
|
200
|
+
| forward edit | undo |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `set #id` | `revert #id` (splice) |
|
|
203
|
+
| `delete #id` | `revert #id` (resurrect) |
|
|
204
|
+
| `add #id` | `revert #id` (remove) — or `delete #id` |
|
|
205
|
+
| `rename #old #new` | `rename #new #old` (self-inverse) |
|
|
206
|
+
|
|
207
|
+
`revert` reads the `.gemlhistory` sidecar, so `set`/`delete`/`add` undo needs a
|
|
208
|
+
prior `geml history save`; `rename` is its own inverse and needs no history.
|
|
209
|
+
|
|
210
|
+
A **heading's** `#id` addresses its whole **section** — the heading line through
|
|
211
|
+
the line before the next heading of the same-or-higher level — so the prose
|
|
212
|
+
under a heading is block-editable with no extra syntax.
|
|
213
|
+
Spans overlap: blocks nested in the section keep their own ids, and a `set` on
|
|
214
|
+
the section that drops one of them is refused by the guard. `get --json` on a
|
|
215
|
+
heading covers the same content as the raw span: a section envelope
|
|
216
|
+
`{kind:"section", id, level, blocks:[heading, …its section's blocks]}` (a
|
|
217
|
+
block/footnote id still prints its single model node). `--head` narrows
|
|
218
|
+
`get`/`set`/`revert` to ANY id's head line — a heading's line, or a typed
|
|
219
|
+
block's opening fence line, so an agent renames a heading or edits a block's
|
|
220
|
+
attributes (caption, compute, …) without touching the body. Convention: keep
|
|
221
|
+
the document title in `=== meta` (`title = "…"`), not an H1 — a lone top-level
|
|
222
|
+
`#` section is the whole document, the telltale that it is really a title.
|
|
223
|
+
|
|
224
|
+
## MCP Server
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
228
|
+
To connect it to an MCP-compatible client, provide the `npx` execution command and specify the `--root` argument (the directory containing your `.geml` files).
|
|
229
|
+
|
|
230
|
+
### Claude Desktop
|
|
231
|
+
Add to your `claude_desktop_config.json`:
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"mcpServers": {
|
|
235
|
+
"geml": {
|
|
236
|
+
"command": "npx",
|
|
237
|
+
"args": [
|
|
238
|
+
"-y",
|
|
239
|
+
"@geml/geml@latest",
|
|
240
|
+
"mcp",
|
|
241
|
+
"--root",
|
|
242
|
+
"/absolute/path/to/your/docs"
|
|
243
|
+
]
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Claude Code / CLI Clients
|
|
250
|
+
Run the following command to add the server:
|
|
251
|
+
```sh
|
|
252
|
+
claude mcp add geml -- npx -y @geml/geml@latest mcp --root /absolute/path/to/your/docs
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
With a code graph under `--root` (`geml codemap build`), the same server also
|
|
256
|
+
serves four read-only `geml_codemap_*` tools. Every tool and option:
|
|
257
|
+
[`docs/mcp-guide.md`](https://github.com/geml-spec/geml/blob/main/docs/mcp-guide.md).
|
|
258
|
+
|
|
259
|
+
## Library
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
import { parse, serialize, renderHtml, gemlToMd, mdToGeml } from "@geml/geml";
|
|
263
|
+
|
|
264
|
+
const doc = parse(src); // { kind:"document", children, ids, diagnostics }
|
|
265
|
+
const ok = !doc.diagnostics.some(d => d.severity === "error");
|
|
266
|
+
const html = renderHtml(doc); // one self-contained HTML string
|
|
267
|
+
const md = gemlToMd(doc).md; // GitHub-Flavored Markdown (lossy)
|
|
268
|
+
const geml = mdToGeml(markdown).geml; // the inverse
|
|
269
|
+
const canonical = serialize(doc); // GEML text; parse(serialize(parse(x))) is stable
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
`parse(src, { resolveDoc })` enables cross-document reference checking — pass a
|
|
273
|
+
function that returns another file's source by path (or `null`).
|
|
274
|
+
|
|
275
|
+
## Documentation
|
|
276
|
+
|
|
277
|
+
Full normative spec, history-sidecar spec, and format comparison live in the
|
|
278
|
+
[repository](https://github.com/geml-spec/geml). The spec is itself
|
|
279
|
+
written in GEML (`GEML-spec.geml`) and parsed clean on every test run.
|
|
280
|
+
|
|
281
|
+
What changed between releases:
|
|
282
|
+
[`CHANGELOG.md`](https://github.com/geml-spec/geml/blob/main/CHANGELOG.md).
|
|
283
|
+
The parser and the specification version independently — `geml --version --json`
|
|
284
|
+
prints both.
|
|
285
|
+
|
|
286
|
+
## License
|
|
287
|
+
|
|
288
|
+
MIT.
|