@geml/geml 1.7.8 → 1.8.2
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 -285
- 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 -431
- 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/attrs.d.ts +8 -0
- package/dist/attrs.js +24 -0
- package/dist/cli.js +190 -139
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.js +5 -1
- package/dist/geml.js +63 -25
- package/dist/inline.d.ts +7 -1
- package/dist/inline.js +203 -137
- package/dist/mcp.js +68 -26
- package/dist/render-html.js +35 -35
- package/dist/render.js +157 -157
- package/package.json +1 -1
- package/skill/SKILL.md +167 -167
- package/skill/references/authoring.geml +369 -365
package/dist/cli.js
CHANGED
|
@@ -125,144 +125,151 @@ function embedSrcOf(source, unit) {
|
|
|
125
125
|
const v = parseAttrs(braces[0]).attrs["src"];
|
|
126
126
|
return typeof v === "string" ? v : undefined;
|
|
127
127
|
}
|
|
128
|
-
const USAGE = `geml — GEML reference CLI
|
|
129
|
-
|
|
130
|
-
Usage:
|
|
131
|
-
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
|
|
132
|
-
(--root widens cross-doc resolution to dir d, as on check — an
|
|
133
|
-
=== embed whose target sits above the file's own directory
|
|
134
|
-
needs it, or it renders unresolved)
|
|
135
|
-
--to <output>: json | html | md | geml
|
|
136
|
-
--to md -> Markdown (lossy)
|
|
137
|
-
--to html -> self-contained HTML
|
|
138
|
-
--to html --fragment -> body-only markup, no page shell
|
|
139
|
-
(embed in your own layout; assets via pageAssets)
|
|
140
|
-
--to geml -> canonical re-format
|
|
141
|
-
--to json -> document-model JSON (default)
|
|
142
|
-
--from <input>: geml | md | json (overrides extension; html is output-only)
|
|
143
|
-
geml notes.md -> GEML (md inferred from extension)
|
|
144
|
-
geml model.json --to geml -> GEML (round-trips a prior --to json)
|
|
145
|
-
geml - --from md read Markdown on stdin
|
|
146
|
-
geml list <file.geml|-> [--json] list every addressable block: address, kind, lines
|
|
147
|
-
(call this first — its addresses are what every verb below takes)
|
|
148
|
-
geml find <pattern> [<file|dir> …] [--json] [--case] [--head] search block content -> file#address
|
|
149
|
-
(an address, not a line number, so a hit pastes into get/set;
|
|
150
|
-
a named file is searched whatever its extension, a dir walks
|
|
151
|
-
*.geml only; exit 1 when nothing matched)
|
|
152
|
-
geml get <file.geml|-> [#id] [--json] [--head|--intro|--body] with #id: print that block
|
|
153
|
-
(a heading id = its whole section; --head = head line;
|
|
154
|
-
--json = model node). Without #id: list all addressable
|
|
155
|
-
ids (--json = array). A selector may also be a POSITION,
|
|
156
|
-
'L27' or 'L27-58' — the smallest block containing those
|
|
157
|
-
lines, which is how a grep hit or a stack trace becomes
|
|
158
|
-
an address.
|
|
159
|
-
geml set <file.geml|-> #id [--head|--intro|--body] [--in f[#src]|-] [-o f] replace ONE block by id
|
|
160
|
-
geml replace <file.geml|-> <old> <new> [--within <selector>] [-o f] EXPERIMENTAL: swap a literal string, checked and reported
|
|
161
|
-
(--in F takes F's block #id, F#src takes #src, else stdin raw;
|
|
162
|
-
default = whole block · --head = head line · --body = body)
|
|
163
|
-
geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
|
|
164
|
-
(1+ blocks and/or prose; content keeps its own ids, a clash is refused)
|
|
165
|
-
geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
|
|
166
|
-
(a missing id is skipped; a dangling reference is a warning, not a refusal)
|
|
167
|
-
geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
|
|
168
|
-
geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
|
|
169
|
-
(sel: 0 | -N | id-prefix | changed; default -1)
|
|
170
|
-
geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
|
|
171
|
-
(--root widens cross-doc refs to dir d, e.g. the repo root
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
geml
|
|
178
|
-
(
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
geml
|
|
183
|
-
(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
128
|
+
const USAGE = `geml — GEML reference CLI
|
|
129
|
+
|
|
130
|
+
Usage:
|
|
131
|
+
geml <file.geml|-> [--to <fmt>] [--from <fmt>] [--root d] [-o out] transform a document (default: --to json)
|
|
132
|
+
(--root widens cross-doc resolution to dir d, as on check — an
|
|
133
|
+
=== embed whose target sits above the file's own directory
|
|
134
|
+
needs it, or it renders unresolved)
|
|
135
|
+
--to <output>: json | html | md | geml
|
|
136
|
+
--to md -> Markdown (lossy)
|
|
137
|
+
--to html -> self-contained HTML
|
|
138
|
+
--to html --fragment -> body-only markup, no page shell
|
|
139
|
+
(embed in your own layout; assets via pageAssets)
|
|
140
|
+
--to geml -> canonical re-format
|
|
141
|
+
--to json -> document-model JSON (default)
|
|
142
|
+
--from <input>: geml | md | json (overrides extension; html is output-only)
|
|
143
|
+
geml notes.md -> GEML (md inferred from extension)
|
|
144
|
+
geml model.json --to geml -> GEML (round-trips a prior --to json)
|
|
145
|
+
geml - --from md read Markdown on stdin
|
|
146
|
+
geml list <file.geml|-> [--json] list every addressable block: address, kind, lines
|
|
147
|
+
(call this first — its addresses are what every verb below takes)
|
|
148
|
+
geml find <pattern> [<file|dir> …] [--json] [--case] [--head] search block content -> file#address
|
|
149
|
+
(an address, not a line number, so a hit pastes into get/set;
|
|
150
|
+
a named file is searched whatever its extension, a dir walks
|
|
151
|
+
*.geml only; exit 1 when nothing matched)
|
|
152
|
+
geml get <file.geml|-> [#id] [--json] [--head|--intro|--body] with #id: print that block
|
|
153
|
+
(a heading id = its whole section; --head = head line;
|
|
154
|
+
--json = model node). Without #id: list all addressable
|
|
155
|
+
ids (--json = array). A selector may also be a POSITION,
|
|
156
|
+
'L27' or 'L27-58' — the smallest block containing those
|
|
157
|
+
lines, which is how a grep hit or a stack trace becomes
|
|
158
|
+
an address.
|
|
159
|
+
geml set <file.geml|-> #id [--head|--intro|--body] [--in f[#src]|-] [-o f] replace ONE block by id
|
|
160
|
+
geml replace <file.geml|-> <old> <new> [--within <selector>] [-o f] EXPERIMENTAL: swap a literal string, checked and reported
|
|
161
|
+
(--in F takes F's block #id, F#src takes #src, else stdin raw;
|
|
162
|
+
default = whole block · --head = head line · --body = body)
|
|
163
|
+
geml add <file.geml|-> (--append | --before #id | --after #id) [--in f[#src]|-] [-o f] insert a fragment
|
|
164
|
+
(1+ blocks and/or prose; content keeps its own ids, a clash is refused)
|
|
165
|
+
geml delete <file.geml|-> #id [#id2 …] [-o f] remove one or more blocks
|
|
166
|
+
(a missing id is skipped; a dangling reference is a warning, not a refusal)
|
|
167
|
+
geml rename <file.geml|-> #old #new [-o f] rename an id and every reference to it (id-boundary safe)
|
|
168
|
+
geml revert <file.geml> #id [--rev <sel>] [--head] undo one block to a past revision (splice / resurrect / remove)
|
|
169
|
+
(sel: 0 | -N | id-prefix | changed; default -1)
|
|
170
|
+
geml check <file.geml|-> [--root d] [--json] validate only: diagnostics + exit code
|
|
171
|
+
(--root widens cross-doc refs to dir d, e.g. the repo root;
|
|
172
|
+
every read and write verb takes it. A write is REFUSED when
|
|
173
|
+
the result would not parse, so a document whose ../x.md
|
|
174
|
+
links only resolve from the repo root needs --root to be
|
|
175
|
+
editable at all — otherwise the guard reads its own blind
|
|
176
|
+
spot as breakage)
|
|
177
|
+
geml history <save|get|restore|verify> <file.geml> [...] .gemlhistory version sidecar
|
|
178
|
+
(save = append the file as a revision · get = list revisions, or
|
|
179
|
+
print one · restore = overwrite the file with one · verify = rebuild
|
|
180
|
+
and re-hash the whole chain)
|
|
181
|
+
geml codemap <build|verify|render|serve|refresh|find> [...] code-graph toolkit (alias: codegraph)
|
|
182
|
+
geml mcp --root <dir> [--graph <dir>] [--no-history] serve documents (and the code graph) over MCP (stdio)
|
|
183
|
+
(11 tools, each geml_ + its CLI command path: list/find/get/check/history/to +
|
|
184
|
+
set/add/delete/rename/revert; every write is validated before it
|
|
185
|
+
reaches disk. A code graph under --root adds four read-only
|
|
186
|
+
geml_codemap_* tools to the same server)
|
|
187
|
+
geml skill install [--dest <dir>] [--no-global] [--no-mcp] set up GEML for Claude Code, user-global
|
|
188
|
+
(authoring skill -> ~/.claude/skills/geml, CLI -> npm i -g,
|
|
189
|
+
MCP server registered at user scope; touches no settings.json,
|
|
190
|
+
installs no hooks; idempotent — re-run to update)
|
|
191
|
+
geml --help | --version [--json]
|
|
192
|
+
|
|
193
|
+
Use '-' as the file to read from stdin.
|
|
194
|
+
Mutations (set/add/delete/rename) write the whole updated document in place for a
|
|
195
|
+
file, or to stdout for '-' input; -o redirects it (-o - = stdout).
|
|
196
|
+
Exit codes:
|
|
197
|
+
0 ok
|
|
198
|
+
1 document/operation error
|
|
199
|
+
2 command usage error.
|
|
195
200
|
`;
|
|
196
201
|
// One-line usage for each subcommand — the single source for both the error
|
|
197
202
|
// shown on misuse and the `<cmd> --help` text.
|
|
198
203
|
const SUBHELP = {
|
|
199
204
|
get: "usage: geml get <file.geml|-> [<selector>] [--head|--intro|--body] [--view [--root <dir>]] [--json] (selector = a filter over blocks: #id | '## Heading' (its whole section) | '=== type' (every block of that type — N matches print N contents, count on stderr) | '=== type@<hex>[~n]' or '@<hex>[~n]' (content address, for blocks with no #id) | L<n> or L<n>-<m> (position — the smallest block that fully contains those lines, so the `L27-58` the listing prints pastes straight back, and a line number from an editor, a linter or a diff hunk becomes a block); a section cuts three ways — --head = the heading line, --intro = its opening region: everything under it up to its FIRST SUBHEADING (empty when one follows immediately, the whole body when none does; a block has no intro and is refused), --body = everything under it; --view = read THROUGH an `embed` to the entity block it stands for, following a chain to its end (the identity on any other block, and on a section selector — it never splices two documents' bytes together); provenance goes to stderr as `view: <sel> -> <doc>[#<id>]`; read-only, `set` refuses it; chain reads are confined to --root (default: the document's own directory) and never fetched over the network; without a selector: list every addressable block with its shortest unique address, --json = array)",
|
|
200
|
-
set: "usage: geml set <file.geml|-> <selector> [--head|--intro|--body] [--in F | --in F#src | --in -] [-o out.geml] (selector as in `get`, but it must match exactly ONE block — '=== type' matching several is refused; content: --in F takes F's block #id, --in F#src takes #src, else stdin raw; default = whole block, --head = head line — both normalize the id when the target has one — --body = body, --intro = a heading's opening region up to its first subheading (an empty region INSERTS there); guarded splice, refused if it breaks the doc — but a replacement that REMOVES blocks is carried out and reported on stderr, named ones and unnamed alike, with `geml revert` as the way back (the same stance `delete` takes; the ordinary read-edit-write cycle removes nothing, since `get` handed those blocks over); writing through an @<hex> address prints the new address on stderr)",
|
|
201
|
-
add: "usage: geml add <file.geml|-> (--append | --before #id | --after #id) [--in F | --in F#src | --in -] [-o out.geml] (insert a GEML fragment — 1+ blocks and/or prose — at a position; --in F takes all of F, --in F#src takes #src, else stdin raw; content keeps its own ids, a collision is refused)",
|
|
202
|
-
delete: "usage: geml delete <file.geml|-> #id [#id2 …] [-o out.geml] (remove one or more blocks; a missing id is skipped with a note, not an error; a reference left dangling is a warning, not a refusal — delete never fails on a live reference)",
|
|
203
|
-
rename: "usage: geml rename <file.geml|-> #old #new [-o out.geml] (rewrite an id's declaration AND every reference — [[#id]], [text](#id), chart data=#id, footnote [^id] — id-boundary safe, skipping raw block bodies; #new must be free; refused if it breaks the doc)",
|
|
205
|
+
set: "usage: geml set <file.geml|-> <selector> [--head|--intro|--body] [--in F | --in F#src | --in -] [-o out.geml] [--root d] (selector as in `get`, but it must match exactly ONE block — '=== type' matching several is refused; content: --in F takes F's block #id, --in F#src takes #src, else stdin raw; default = whole block, --head = head line — both normalize the id when the target has one — --body = body, --intro = a heading's opening region up to its first subheading (an empty region INSERTS there); guarded splice, refused if it breaks the doc — but a replacement that REMOVES blocks is carried out and reported on stderr, named ones and unnamed alike, with `geml revert` as the way back (the same stance `delete` takes; the ordinary read-edit-write cycle removes nothing, since `get` handed those blocks over); writing through an @<hex> address prints the new address on stderr)",
|
|
206
|
+
add: "usage: geml add <file.geml|-> (--append | --before #id | --after #id) [--in F | --in F#src | --in -] [-o out.geml] [--root d] (insert a GEML fragment — 1+ blocks and/or prose — at a position; --in F takes all of F, --in F#src takes #src, else stdin raw; content keeps its own ids, a collision is refused)",
|
|
207
|
+
delete: "usage: geml delete <file.geml|-> #id [#id2 …] [-o out.geml] [--root d] (remove one or more blocks; a missing id is skipped with a note, not an error; a reference left dangling is a warning, not a refusal — delete never fails on a live reference)",
|
|
208
|
+
rename: "usage: geml rename <file.geml|-> #old #new [-o out.geml] [--root d] (rewrite an id's declaration AND every reference — [[#id]], [text](#id), chart data=#id, footnote [^id] — id-boundary safe, skipping raw block bodies; #new must be free; refused if it breaks the doc)",
|
|
204
209
|
list: "usage: geml list <file.geml|-> [--json] (list every addressable block with its shortest unique address, its kind and its line range — the same listing `geml get <file>` prints with no selector, under the name the MCP surface already uses. Call it FIRST: the addresses it prints are what get/set/add/delete/rename/revert all take)",
|
|
205
210
|
find: "usage: geml find <pattern> [<file|dir> …] [--json] [--case] [--head] (search block CONTENT and print `<file>TAB<address>` per hit — an address, never a line number, so a hit is `geml get <file> '<address>'` with no editing. The address is the INNERMOST block holding the match, never its enclosing section, and a block is reported once however many lines in it matched. Substring, case-insensitive unless --case; a file you NAME is searched whatever its extension, including Markdown, while a directory is walked for *.geml only; no path = the current directory; --head adds the matching line as a third column. Exit 1 when nothing matched, so `if geml find …` works in a script)",
|
|
206
|
-
replace: "usage: geml replace <file.geml|-> <old> <new> [--within <selector>] [-o out.geml] (EXPERIMENTAL — this verb MAY BE WITHDRAWN in a later release; it is here to find out whether an addressed, checked replacement earns its place beside `sed`, and if it does not, it goes. Build nothing on it you cannot change, and say so in a discussion if it is doing real work for you. Swaps a LITERAL string — never a pattern, that is what `sed` is for and where the footguns are. Without --within the whole document; with it, only inside the blocks that selector matches, and unlike `set` it may match several: `--within '=== table'` means every table. What this buys over `sed -i`, at the same cost of two short strings and nothing read: the result is re-parsed and refused if it would break the document, the blocks it touched are NAMED on stderr, and the write lands in .gemlhistory where `revert` can undo it. An id is not text — a replacement that would rename one is refused and points at `geml rename`, which fixes every reference too. Exit 1 when nothing matched, so `if geml replace …` works in a script)",
|
|
211
|
+
replace: "usage: geml replace <file.geml|-> <old> <new> [--within <selector>] [-o out.geml] [--root d] (EXPERIMENTAL — this verb MAY BE WITHDRAWN in a later release; it is here to find out whether an addressed, checked replacement earns its place beside `sed`, and if it does not, it goes. Build nothing on it you cannot change, and say so in a discussion if it is doing real work for you. Swaps a LITERAL string — never a pattern, that is what `sed` is for and where the footguns are. Without --within the whole document; with it, only inside the blocks that selector matches, and unlike `set` it may match several: `--within '=== table'` means every table. What this buys over `sed -i`, at the same cost of two short strings and nothing read: the result is re-parsed and refused if it would break the document, the blocks it touched are NAMED on stderr, and the write lands in .gemlhistory where `revert` can undo it. An id is not text — a replacement that would rename one is refused and points at `geml rename`, which fixes every reference too. Exit 1 when nothing matched, so `if geml replace …` works in a script)",
|
|
207
212
|
check: "usage: geml check <file.geml|-> [--root <dir>] [--json] (--root: resolve cross-doc refs within <dir> instead of the file's own directory)",
|
|
208
|
-
revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--append|--before #x|--after #x] [--head] [--dry-run] [-o out] (reconcile #id to a revision: splice / resurrect / remove; sel: 0 | -N | id-prefix | changed; default -1)",
|
|
209
|
-
history: `usage: geml history save <file.geml> [-m <msg>] append the working file as a new revision (identical to the tip = no-op)
|
|
210
|
-
geml history get <file.geml> [<rev>] [--json] NO <rev>: every revision, newest first, first column = the selector; WITH <rev>: that revision's full text
|
|
211
|
-
geml history restore <file.geml> <rev> [--force] overwrite the working file with a revision (--force discards unsaved changes)
|
|
212
|
-
geml history verify <file.geml> rebuild and re-hash every revision in the chain
|
|
213
|
-
(<rev>: 0 = the tip | -N = N revisions back | an unambiguous revision id — the strings 'get' prints.
|
|
213
|
+
revert: "usage: geml revert <file.geml> #id [--rev <sel>] [--append|--before #x|--after #x] [--head] [--dry-run] [-o out] [--root d] (reconcile #id to a revision: splice / resurrect / remove; sel: 0 | -N | id-prefix | changed; default -1)",
|
|
214
|
+
history: `usage: geml history save <file.geml> [-m <msg>] append the working file as a new revision (identical to the tip = no-op)
|
|
215
|
+
geml history get <file.geml> [<rev>] [--json] NO <rev>: every revision, newest first, first column = the selector; WITH <rev>: that revision's full text
|
|
216
|
+
geml history restore <file.geml> <rev> [--force] overwrite the working file with a revision (--force discards unsaved changes)
|
|
217
|
+
geml history verify <file.geml> rebuild and re-hash every revision in the chain
|
|
218
|
+
(<rev>: 0 = the tip | -N = N revisions back | an unambiguous revision id — the strings 'get' prints.
|
|
214
219
|
All four take --history <path> to point at a sidecar other than <file>.gemlhistory.)`,
|
|
215
|
-
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
216
|
-
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
217
|
-
geml codemap verify [dir] geml check + profile reference checks
|
|
218
|
-
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
219
|
-
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
220
|
-
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
221
|
-
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
220
|
+
codemap: `usage: geml codemap build [--root <repo>] # auto-detect languages, run the indexer(s), and merge into one codemap (--root defaults to the current directory)
|
|
221
|
+
geml codemap build (--db <graph.db> | --adapter joern|scip --raw <in>)+ [--root <repo>] [--out .geml-code-graph] [--container module|dir|file] [--lang <JAVASRC|NEWC|…>] [--joern <path>] [--history [-m msg]]
|
|
222
|
+
geml codemap verify [dir] geml check + profile reference checks
|
|
223
|
+
geml codemap render [dir] every doc -> sibling .html (open index.html from disk)
|
|
224
|
+
geml codemap serve [dir] [--port 8140] [--watch] [--background|--stop] live viewer: pages render from .geml on request; --watch re-runs the recipe when sources change
|
|
225
|
+
geml codemap refresh [dir] [--force] [--commit] [--background|--hook] re-run the recorded build recipe (_index/refresh.json); --commit lands it as its own commit
|
|
226
|
+
geml codemap find <name> [dir] locate a symbol by substring name -> doc#id + src (stdout, no browser)
|
|
222
227
|
(<dir> for verify/render/serve/refresh/find defaults to ./.geml-code-graph; codegraph and code-graph are accepted as aliases of codemap)`,
|
|
223
|
-
mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
224
|
-
|
|
225
|
-
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
|
|
226
|
-
Every tool is geml_ + its CLI COMMAND PATH, so the terminal and the assistant
|
|
227
|
-
share one vocabulary — geml_history mirrors the "geml history" command group,
|
|
228
|
-
whose read verb (get) is the only one of the four served here.
|
|
229
|
-
Eleven tools: geml_list · geml_find · geml_get · geml_check · geml_history
|
|
230
|
-
geml_to · geml_set · geml_add · geml_delete · geml_rename
|
|
231
|
-
geml_revert
|
|
232
|
-
With a code graph under --root, four more (read-only), so one client entry
|
|
233
|
-
covers both: geml_codemap_search · geml_codemap_callchain
|
|
234
|
-
geml_codemap_list · geml_codemap_node
|
|
235
|
-
|
|
236
|
-
--root <dir> REQUIRED. Root holding the .geml documents. Every path a
|
|
237
|
-
client names is confined here; a client cannot widen it.
|
|
238
|
-
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
239
|
-
<root>/.geml-code-graph when it holds an index.geml; with
|
|
240
|
-
no graph the four graph tools are not served at all.
|
|
241
|
-
--no-history Skip the .gemlhistory revision saved before each write
|
|
242
|
-
(default: save one, so geml_revert always has a revision
|
|
243
|
-
to undo to).
|
|
244
|
-
|
|
245
|
-
Register with a client:
|
|
228
|
+
mcp: `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
|
|
229
|
+
|
|
230
|
+
Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0).
|
|
231
|
+
Every tool is geml_ + its CLI COMMAND PATH, so the terminal and the assistant
|
|
232
|
+
share one vocabulary — geml_history mirrors the "geml history" command group,
|
|
233
|
+
whose read verb (get) is the only one of the four served here.
|
|
234
|
+
Eleven tools: geml_list · geml_find · geml_get · geml_check · geml_history
|
|
235
|
+
geml_to · geml_set · geml_add · geml_delete · geml_rename
|
|
236
|
+
geml_revert
|
|
237
|
+
With a code graph under --root, four more (read-only), so one client entry
|
|
238
|
+
covers both: geml_codemap_search · geml_codemap_callchain
|
|
239
|
+
geml_codemap_list · geml_codemap_node
|
|
240
|
+
|
|
241
|
+
--root <dir> REQUIRED. Root holding the .geml documents. Every path a
|
|
242
|
+
client names is confined here; a client cannot widen it.
|
|
243
|
+
--graph <dir> Code-graph directory, inside --root. Defaults to
|
|
244
|
+
<root>/.geml-code-graph when it holds an index.geml; with
|
|
245
|
+
no graph the four graph tools are not served at all.
|
|
246
|
+
--no-history Skip the .gemlhistory revision saved before each write
|
|
247
|
+
(default: save one, so geml_revert always has a revision
|
|
248
|
+
to undo to).
|
|
249
|
+
|
|
250
|
+
Register with a client:
|
|
246
251
|
claude mcp add geml -- geml mcp --root /abs/path/to/repo`,
|
|
247
|
-
skill: `usage: geml skill install [--dest <skillsDir>] [--no-global] [--no-mcp] [--dry-run]
|
|
248
|
-
|
|
249
|
-
One command, three things, all user-global — so any Claude Code session can
|
|
250
|
-
author, validate, and blockwise-edit GEML:
|
|
251
|
-
1. the authoring skill -> <skillsDir>/geml (default ~/.claude/skills/geml)
|
|
252
|
-
2. the geml CLI -> npm i -g @geml/geml (skipped when already
|
|
253
|
-
3. the MCP server -> claude mcp add --scope user geml -- npx -y @geml/geml mcp --root .
|
|
254
|
-
Touches no settings.json and installs no hooks. Idempotent —
|
|
255
|
-
upgrade
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
--
|
|
260
|
-
--
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
252
|
+
skill: `usage: geml skill install [--dest <skillsDir>] [--no-global] [--no-mcp] [--dry-run]
|
|
253
|
+
|
|
254
|
+
One command, three things, all user-global — so any Claude Code session can
|
|
255
|
+
author, validate, and blockwise-edit GEML:
|
|
256
|
+
1. the authoring skill -> <skillsDir>/geml (default ~/.claude/skills/geml)
|
|
257
|
+
2. the geml CLI -> npm i -g @geml/geml@<this version> (skipped when PATH already has it)
|
|
258
|
+
3. the MCP server -> claude mcp add --scope user geml -- npx -y @geml/geml mcp --root .
|
|
259
|
+
Touches no settings.json and installs no hooks. Idempotent — and it is the
|
|
260
|
+
whole upgrade: a re-run refreshes the skill text AND brings the global CLI to
|
|
261
|
+
the version that text documents, so "npx -y @geml/geml skill install" is one
|
|
262
|
+
step, not two.
|
|
263
|
+
|
|
264
|
+
--dest <dir> install the skill under <dir> instead of ~/.claude/skills
|
|
265
|
+
--no-global leave the global CLI alone — no install, no version change
|
|
266
|
+
--no-mcp skip the MCP server registration
|
|
267
|
+
--dry-run report what would be written, change nothing
|
|
268
|
+
|
|
269
|
+
Other agent tools are installed by DETECTION: a tool's own context file gets
|
|
270
|
+
the skill text inside a marker pair (refreshed on a re-run, nothing else in
|
|
271
|
+
the file touched) when its directory is already there — ~/.gemini, ~/.qwen,
|
|
272
|
+
and an AGENTS.md in the current project. A tool that is not installed is
|
|
266
273
|
skipped and named; no tool directory is ever created for you.`,
|
|
267
274
|
};
|
|
268
275
|
// Set from argv at dispatch time; when true, errors are emitted as a JSON
|
|
@@ -426,7 +433,27 @@ function existsFor(file, root) {
|
|
|
426
433
|
}
|
|
427
434
|
// Both halves for a parse: every call site wants them together, and pairing
|
|
428
435
|
// them here keeps a resolver from being wired up without its existence probe.
|
|
429
|
-
|
|
436
|
+
// The root that cross-document references resolve against for the rest of this
|
|
437
|
+
// process, taken from `--root` by whichever verb is running.
|
|
438
|
+
//
|
|
439
|
+
// Why a module-level value rather than a parameter: the write verbs re-parse
|
|
440
|
+
// through `resolveSelector`, `spliceSpan`, `insertFragment`, `rewriteId` and
|
|
441
|
+
// `unitNode`, and threading a root through those signatures and their dozen call
|
|
442
|
+
// sites would deliver the same value to the same place by a longer route. It is
|
|
443
|
+
// safe to hold here because a verb is one process: `geml mcp` spawns the CLI per
|
|
444
|
+
// tool call rather than calling these functions in-process.
|
|
445
|
+
let CLI_ROOT;
|
|
446
|
+
// Read `--root` and make it this process's root. Verbs call this before they
|
|
447
|
+
// parse anything; `--root` present with no directory is a usage error, the same
|
|
448
|
+
// as it is on `check`.
|
|
449
|
+
function useRoot(args) {
|
|
450
|
+
const r = flag(args, "--root");
|
|
451
|
+
if (args.includes("--root") && r === undefined)
|
|
452
|
+
fail("--root needs a directory", 2);
|
|
453
|
+
CLI_ROOT = r;
|
|
454
|
+
return r;
|
|
455
|
+
}
|
|
456
|
+
function docOpts(file, root = CLI_ROOT) {
|
|
430
457
|
return { resolveDoc: resolverFor(file, root), docExists: existsFor(file, root) };
|
|
431
458
|
}
|
|
432
459
|
// `geml check <file>` — validate only: diagnostics + exit code, no document
|
|
@@ -1088,6 +1115,7 @@ function reportMatches(type, units) {
|
|
|
1088
1115
|
// descriptions agents are already reading. `get <file>` keeps working.
|
|
1089
1116
|
function runList(args) {
|
|
1090
1117
|
const [file, extra] = positionals(args, ["--root"]);
|
|
1118
|
+
useRoot(args);
|
|
1091
1119
|
if (!file)
|
|
1092
1120
|
fail(SUBHELP.list);
|
|
1093
1121
|
// `list` IS the empty filter, so a selector here means the caller wanted
|
|
@@ -1135,7 +1163,10 @@ function gemlFilesUnder(path, out, explicit = false) {
|
|
|
1135
1163
|
// being true the moment anything above it changes. `codemap find` already
|
|
1136
1164
|
// resolves a substring to `doc#id` for symbols; this is the same move for prose.
|
|
1137
1165
|
function runFind(args) {
|
|
1138
|
-
|
|
1166
|
+
// `--root` is declared here, and ignored, so that it cannot be mistaken for
|
|
1167
|
+
// one more path to search: `find` resolves no cross-document references, and
|
|
1168
|
+
// swallowing the directory as a search path widens what a caller narrowed.
|
|
1169
|
+
const pos = positionals(args, ["--root"]);
|
|
1139
1170
|
const pattern = pos[0];
|
|
1140
1171
|
if (pattern === undefined)
|
|
1141
1172
|
fail(SUBHELP.find);
|
|
@@ -1220,6 +1251,7 @@ function runGet(args) {
|
|
|
1220
1251
|
const introOnly = args.includes("--intro");
|
|
1221
1252
|
const view = args.includes("--view");
|
|
1222
1253
|
const [file, rawSel] = positionals(args, ["--root"]);
|
|
1254
|
+
useRoot(args);
|
|
1223
1255
|
if (!file)
|
|
1224
1256
|
fail(SUBHELP.get);
|
|
1225
1257
|
const parts = [headOnly && "--head", introOnly && "--intro", bodyOnly && "--body"].filter(Boolean);
|
|
@@ -1339,7 +1371,8 @@ function runGet(args) {
|
|
|
1339
1371
|
function runReplace(args) {
|
|
1340
1372
|
const out = flag(args, "-o") ?? flag(args, "--out");
|
|
1341
1373
|
const within = flag(args, "--within");
|
|
1342
|
-
const [file, oldText, newText] = positionals(args, ["-o", "--out", "--within"]);
|
|
1374
|
+
const [file, oldText, newText] = positionals(args, ["-o", "--out", "--within", "--root"]);
|
|
1375
|
+
useRoot(args);
|
|
1343
1376
|
if (!file || oldText === undefined || newText === undefined)
|
|
1344
1377
|
fail(SUBHELP.replace);
|
|
1345
1378
|
if (oldText === "")
|
|
@@ -1474,7 +1507,8 @@ function runSet(args) {
|
|
|
1474
1507
|
if (args.includes("--view")) {
|
|
1475
1508
|
fail("--view is read-only. To edit the target, read the frame's `src` and edit that document.", 2);
|
|
1476
1509
|
}
|
|
1477
|
-
const [file, rawSel] = positionals(args, ["-o", "--out", "--in"]);
|
|
1510
|
+
const [file, rawSel] = positionals(args, ["-o", "--out", "--in", "--root"]);
|
|
1511
|
+
useRoot(args);
|
|
1478
1512
|
if (!file)
|
|
1479
1513
|
fail(SUBHELP.set);
|
|
1480
1514
|
// No selector: there is no block to replace. Point the way to discovery, not a
|
|
@@ -1649,7 +1683,8 @@ function runAdd(args) {
|
|
|
1649
1683
|
const posCount = (append ? 1 : 0) + (before !== undefined ? 1 : 0) + (after !== undefined ? 1 : 0);
|
|
1650
1684
|
if (posCount !== 1)
|
|
1651
1685
|
fail("add needs exactly one position: --append | --before #id | --after #id", 2);
|
|
1652
|
-
const [file] = positionals(args, ["-o", "--out", "--in", "--before", "--after"]);
|
|
1686
|
+
const [file] = positionals(args, ["-o", "--out", "--in", "--before", "--after", "--root"]);
|
|
1687
|
+
useRoot(args);
|
|
1653
1688
|
if (!file)
|
|
1654
1689
|
fail(SUBHELP.add);
|
|
1655
1690
|
const rawChannel = from === undefined || from === "-";
|
|
@@ -1728,7 +1763,8 @@ function insertFragment(source, lines, at, fragment, file) {
|
|
|
1728
1763
|
// the UNION of target lines, so a line is never spliced twice.
|
|
1729
1764
|
function runDelete(args) {
|
|
1730
1765
|
const out = flag(args, "-o") ?? flag(args, "--out");
|
|
1731
|
-
const pos = positionals(args, ["-o", "--out"]);
|
|
1766
|
+
const pos = positionals(args, ["-o", "--out", "--root"]);
|
|
1767
|
+
useRoot(args);
|
|
1732
1768
|
const file = pos[0];
|
|
1733
1769
|
if (!file)
|
|
1734
1770
|
fail(SUBHELP.delete);
|
|
@@ -1767,7 +1803,8 @@ function runDelete(args) {
|
|
|
1767
1803
|
// free; the guarded re-parse refuses anything that would break the doc.
|
|
1768
1804
|
function runRename(args) {
|
|
1769
1805
|
const out = flag(args, "-o") ?? flag(args, "--out");
|
|
1770
|
-
const [file, rawOld, rawNew] = positionals(args, ["-o", "--out"]);
|
|
1806
|
+
const [file, rawOld, rawNew] = positionals(args, ["-o", "--out", "--root"]);
|
|
1807
|
+
useRoot(args);
|
|
1771
1808
|
if (!file || !rawOld || !rawNew)
|
|
1772
1809
|
fail(SUBHELP.rename);
|
|
1773
1810
|
const oldId = rawOld.replace(/^#/, "");
|
|
@@ -2051,7 +2088,8 @@ function runRevert(args) {
|
|
|
2051
2088
|
if ((append ? 1 : 0) + (before !== undefined ? 1 : 0) + (after !== undefined ? 1 : 0) > 1) {
|
|
2052
2089
|
fail("revert takes at most one position: --append | --before #id | --after #id", 2);
|
|
2053
2090
|
}
|
|
2054
|
-
const [file, rawId] = positionals(args, ["--rev", "--history", "-o", "--out", "--before", "--after"]);
|
|
2091
|
+
const [file, rawId] = positionals(args, ["--rev", "--history", "-o", "--out", "--before", "--after", "--root"]);
|
|
2092
|
+
useRoot(args);
|
|
2055
2093
|
if (!file || !rawId)
|
|
2056
2094
|
fail(SUBHELP.revert);
|
|
2057
2095
|
if (file === "-")
|
|
@@ -2428,13 +2466,26 @@ function runSkill(args) {
|
|
|
2428
2466
|
const sh = process.platform === "win32";
|
|
2429
2467
|
const run = (cmd, a, inherit = false) => spawnSync(cmd, a, { shell: sh, encoding: "utf8", ...(inherit ? { stdio: "inherit" } : {}) });
|
|
2430
2468
|
if (!noGlobal) {
|
|
2469
|
+
// The skill text just written came out of THIS package (`skill/` next to
|
|
2470
|
+
// dist/), so the CLI it teaches is this package's version. Anything else on
|
|
2471
|
+
// PATH is a mismatch — which is why the test is an equality and not an
|
|
2472
|
+
// ordering: "different version" and "not installed" both mean "put the
|
|
2473
|
+
// version the skill documents there", and pinning the spec is what makes
|
|
2474
|
+
// one `npx -y @geml/geml skill install` a complete upgrade.
|
|
2475
|
+
//
|
|
2476
|
+
// The version is read back out of `geml --version` ("geml 1.2.3 (GEML spec
|
|
2477
|
+
// …)") rather than trusted as a whole string: an unreadable or unexpected
|
|
2478
|
+
// line is simply not a match, and lands in the install branch.
|
|
2431
2479
|
const have = run("geml", ["--version"]);
|
|
2432
|
-
|
|
2433
|
-
|
|
2480
|
+
const onPath = have.status === 0
|
|
2481
|
+
? /\b\d+\.\d+\.\d+[^\s)]*/.exec(String(have.stdout ?? ""))?.[0]
|
|
2482
|
+
: undefined;
|
|
2483
|
+
if (onPath === PARSER_VERSION) {
|
|
2484
|
+
console.log(`cli ${PARSER_VERSION} already on PATH`);
|
|
2434
2485
|
}
|
|
2435
2486
|
else {
|
|
2436
|
-
console.log(
|
|
2437
|
-
const r = run("npm", ["install", "-g",
|
|
2487
|
+
console.log(`cli installing @geml/geml globally (npm i -g)${onPath ? `, ${onPath} -> ${PARSER_VERSION}` : ""}...`);
|
|
2488
|
+
const r = run("npm", ["install", "-g", `@geml/geml@${PARSER_VERSION}`, "--no-audit", "--no-fund", "--loglevel=error"], true);
|
|
2438
2489
|
if (r.status !== 0)
|
|
2439
2490
|
console.error("cli global install failed — install later with: npm i -g @geml/geml");
|
|
2440
2491
|
}
|
package/dist/diagnostics.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "stray-labeled-fence" | "fence-like-line" | "unresolvable-code-source" | "bad-code-source" | "bad-source-range" | "
|
|
1
|
+
export type DiagnosticCode = "unterminated-block" | "unknown-block-type" | "unknown-attribute" | "block-nesting-too-deep" | "list-nesting-too-deep" | "inline-nesting-too-deep" | "stray-labeled-fence" | "fence-like-line" | "unresolvable-code-source" | "bad-code-source" | "bad-source-range" | "code-src-and-body" | "name-not-a-name" | "duplicate-id" | "unresolved-reference" | "unresolved-footnote" | "unresolved-cross-document-reference" | "unresolvable-document" | "unchecked-cross-document-reference" | "embed-missing-src" | "ignored-embed-body" | "transclusion-cycle" | "embed-target-not-geml" | "media-target-is-document" | "inline-transclusion-not-inline" | "unsafe-embed-scheme" | "unresolvable-table-source" | "table-source-not-a-table" | "unknown-metadata-reference" | "duplicate-meta-key" | "table-src-and-body" | "unknown-table-format" | "bad-table-delimiter" | "ignored-table-delimiter" | "bad-compute-formula" | "unlexable-compute-formula" | "compute-error" | "compute-non-numeric-cell" | "compute-not-a-number" | "bad-summary-entry" | "summary-unknown-column" | "unlexable-summary-expression" | "summary-error" | "unknown-diagram-format" | "ignored-diagram-body" | "code-graph-missing-src" | "code-graph-unresolvable-document" | "chart-missing-data" | "chart-data-not-a-table" | "chart-missing-type" | "chart-unknown-type" | "chart-unknown-rows-scope" | "chart-missing-channel" | "chart-empty-channel" | "chart-unknown-column" | "chart-unused-channel" | "chart-missing-summary-row" | "chart-summary-row-unavailable" | "chart-non-numeric-value" | "chart-data-not-records" | "data-parse" | "unknown-data-format" | "data-format-no-engine" | "bad-data-schema" | "data-src-and-body" | "bad-data-source" | "unresolvable-data-source";
|
|
2
2
|
export interface Diagnostic {
|
|
3
3
|
severity: "error" | "warning";
|
|
4
4
|
code: DiagnosticCode;
|
package/dist/diagnostics.js
CHANGED
|
@@ -24,7 +24,10 @@ export const SEVERITY = {
|
|
|
24
24
|
"unresolvable-code-source": "warning",
|
|
25
25
|
"bad-code-source": "error",
|
|
26
26
|
"bad-source-range": "error",
|
|
27
|
-
"
|
|
27
|
+
"code-src-and-body": "error",
|
|
28
|
+
// A warning, not an error: this has always parsed, and documents rely on the
|
|
29
|
+
// leniency. What the author is missing is that it parsed as something else.
|
|
30
|
+
"name-not-a-name": "warning",
|
|
28
31
|
"duplicate-id": "error",
|
|
29
32
|
"unresolved-reference": "error",
|
|
30
33
|
"unresolved-footnote": "error",
|
|
@@ -41,6 +44,7 @@ export const SEVERITY = {
|
|
|
41
44
|
"unresolvable-table-source": "error",
|
|
42
45
|
"table-source-not-a-table": "error",
|
|
43
46
|
"unknown-metadata-reference": "error",
|
|
47
|
+
"duplicate-meta-key": "warning",
|
|
44
48
|
"table-src-and-body": "error",
|
|
45
49
|
"unknown-table-format": "warning",
|
|
46
50
|
"bad-table-delimiter": "error",
|