@geml/geml 1.5.1 → 1.6.0

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/dist/history.d.ts CHANGED
@@ -27,14 +27,14 @@ interface History {
27
27
  }
28
28
  /** Reconstruct the content of revision `targetId`. */
29
29
  export declare function reconstruct(h: History, targetId: string): string;
30
- export interface CommitOpts {
30
+ export interface SaveOpts {
31
31
  gemlPath: string;
32
32
  historyPath: string;
33
33
  summary: string;
34
34
  author?: string;
35
35
  at?: Date;
36
36
  }
37
- export declare function commit(o: CommitOpts): {
37
+ export declare function save(o: SaveOpts): {
38
38
  id: string;
39
39
  hash: string;
40
40
  };
@@ -63,7 +63,9 @@ export interface RevisionInfo {
63
63
  current: boolean;
64
64
  }
65
65
  /** Is the working file byte-identical to the sidecar's tip revision? False
66
- * means uncommitted drift (e.g. an earlier commit attempt was refused). */
66
+ * means unsaved drift (e.g. an earlier save was refused by the round-trip
67
+ * gate). Both write paths gate on this so a no-change `save` appends nothing:
68
+ * `geml mcp` before each write (mcp.ts) and `geml history save` (geml.ts). */
67
69
  export declare function isCurrent(historyPath: string, gemlPath: string): boolean;
68
70
  /** Revisions newest-first, each tagged with the `-N` offset that selects it. */
69
71
  export declare function listRevisions(historyPath: string): RevisionInfo[];
@@ -74,11 +76,12 @@ export declare function listRevisions(historyPath: string): RevisionInfo[];
74
76
  *
75
77
  * There is exactly one selector grammar — `0` (the tip), `-N` (N revisions
76
78
  * back), or an unambiguous revision id (prefix, suffix, or exact) — and it is
77
- * the grammar `history log` prints in its first column, so its output is
78
- * copy-pasteable into `revert --rev`, `history show`, and `history restore`
79
- * alike. Keeping this in one function is what makes that true: it used to be
80
- * written twice, and the copy in `restore` never grew the `0`/`-N` arm, so the
81
- * selectors `history log` advertised were rejected by `history show`. */
79
+ * the grammar `history get` prints in its first column, so its output is
80
+ * copy-pasteable into `revert --rev`, `history get <rev>`, and
81
+ * `history restore` alike. Keeping this in one function is what makes that
82
+ * true: it used to be written twice, and the copy in `restore` never grew the
83
+ * `0`/`-N` arm, so the selectors the revision list advertised were rejected by
84
+ * the command that printed a revision. */
82
85
  export declare function resolveRevision(h: History, selector: string): string;
83
86
  export declare function resolveContent(historyPath: string, selector: string): {
84
87
  id: string;
package/dist/history.js CHANGED
@@ -1,9 +1,9 @@
1
- // GEML History extension — commit / restore / verify.
1
+ // GEML History extension — save / restore / verify.
2
2
  //
3
3
  // Implements the `.gemlhistory` companion spec: a self-contained, reverse-delta
4
4
  // version history beside the live `.geml` file. The history file is itself a
5
5
  // GEML document (meta + keyframe + revision + blob blocks). Reverse patches and
6
- // hashes are tool-generated here; every commit re-applies its reverse patch and
6
+ // hashes are tool-generated here; every save re-applies its reverse patch and
7
7
  // asserts a byte-exact round-trip before writing (the spec's verify gate).
8
8
  //
9
9
  // Revision id = `<YYYYMMDDTHHMMSSZ>-<first 8 hex of the version content hash>`.
@@ -83,7 +83,7 @@ function fenceFor(contentLf) {
83
83
  // Unit-key = `#id` (explicit), or `@<8hex content hash>` (derived), with `~n`
84
84
  // disambiguating equal keys by document-order occurrence (§4). `~n` on an #id
85
85
  // key only arises for OUT-OF-SPEC documents that repeat an id — without it the
86
- // key is ambiguous, reverse-patch ops hit the wrong occurrence, and commit()'s
86
+ // key is ambiguous, reverse-patch ops hit the wrong occurrence, and save()'s
87
87
  // round-trip gate (correctly) aborts. Well-formed documents never emit it.
88
88
  const KEY = String.raw `(#[A-Za-z][A-Za-z0-9_-]*(?:~\d+)?|@[0-9a-f]+(?:~\d+)?)`;
89
89
  function sha8(s) {
@@ -267,7 +267,7 @@ function applyReverse(textLf, ops, blobs) {
267
267
  *
268
268
  * If keys were ever non-unique (no public entry point produces that — the only
269
269
  * caller, diffReverse, feeds keyedUnits output), posInB keeps b's LAST index
270
- * per key and the LIS still yields *a* valid monotonic matching; commit()'s
270
+ * per key and the LIS still yields *a* valid monotonic matching; save()'s
271
271
  * byte-exact round-trip gate rejects any diff that fails to reproduce the
272
272
  * parent regardless. */
273
273
  function lcsMatch(a, b) {
@@ -478,7 +478,7 @@ function renderHistory(h, baseName) {
478
478
  }
479
479
  return parts.join("\n");
480
480
  }
481
- export function commit(o) {
481
+ export function save(o) {
482
482
  const { lf: working, nl } = loadBytes(o.gemlPath);
483
483
  const hash = fullHash(working, nl);
484
484
  const stamp = stampUTC(o.at ?? new Date());
@@ -494,10 +494,10 @@ export function commit(o) {
494
494
  const blobMap = new Map(patch.blobs.map((b) => [b.id, b.payload]));
495
495
  const back = applyReverse(working, patch.ops, blobMap);
496
496
  if (bytesOf(back, nl).compare(bytesOf(prevContent, nl)) !== 0) {
497
- throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting commit");
497
+ throw new Error("history: reverse patch does NOT round-trip to the previous revision; aborting save");
498
498
  }
499
- // Blob ids are minted per-diff (b1, b2, …). Renumber this commit's blobs to
500
- // start past the highest id already stored, so a later commit never reuses
499
+ // Blob ids are minted per-diff (b1, b2, …). Renumber this save's blobs to
500
+ // start past the highest id already stored, so a later save never reuses
501
501
  // an earlier revision's blob id — an overwrite in the shared store silently
502
502
  // corrupts reconstruction of older revisions, whose `replace … <- blob:bN`
503
503
  // would then resolve to the wrong (newer) content.
@@ -597,7 +597,9 @@ export function restore(o) {
597
597
  if (existsSync(o.gemlPath)) {
598
598
  const { lf, nl } = loadBytes(o.gemlPath);
599
599
  if (fullHash(lf, nl) !== h.revisions.get(h.current).hash && !o.force) {
600
- throw new Error("history: uncommitted changes in doc.geml; rerun with force to discard them, or commit first");
600
+ // "save first" names the live verb: this string used to say `commit`,
601
+ // which the four-verb collapse removed (design §2).
602
+ throw new Error("history: uncommitted changes in doc.geml; rerun with force to discard them, or save first");
601
603
  }
602
604
  }
603
605
  // destructive linear truncation to `target`
@@ -623,7 +625,9 @@ export function restore(o) {
623
625
  return content;
624
626
  }
625
627
  /** Is the working file byte-identical to the sidecar's tip revision? False
626
- * means uncommitted drift (e.g. an earlier commit attempt was refused). */
628
+ * means unsaved drift (e.g. an earlier save was refused by the round-trip
629
+ * gate). Both write paths gate on this so a no-change `save` appends nothing:
630
+ * `geml mcp` before each write (mcp.ts) and `geml history save` (geml.ts). */
627
631
  export function isCurrent(historyPath, gemlPath) {
628
632
  const h = parseHistory(historyPath);
629
633
  const tip = h.revisions.get(h.current);
@@ -647,11 +651,12 @@ export function listRevisions(historyPath) {
647
651
  *
648
652
  * There is exactly one selector grammar — `0` (the tip), `-N` (N revisions
649
653
  * back), or an unambiguous revision id (prefix, suffix, or exact) — and it is
650
- * the grammar `history log` prints in its first column, so its output is
651
- * copy-pasteable into `revert --rev`, `history show`, and `history restore`
652
- * alike. Keeping this in one function is what makes that true: it used to be
653
- * written twice, and the copy in `restore` never grew the `0`/`-N` arm, so the
654
- * selectors `history log` advertised were rejected by `history show`. */
654
+ * the grammar `history get` prints in its first column, so its output is
655
+ * copy-pasteable into `revert --rev`, `history get <rev>`, and
656
+ * `history restore` alike. Keeping this in one function is what makes that
657
+ * true: it used to be written twice, and the copy in `restore` never grew the
658
+ * `0`/`-N` arm, so the selectors the revision list advertised were rejected by
659
+ * the command that printed a revision. */
655
660
  export function resolveRevision(h, selector) {
656
661
  const off = /^(0|-\d+)$/.exec(selector);
657
662
  if (off) {
package/dist/mcp.d.ts CHANGED
@@ -22,5 +22,5 @@ export declare function allTools(): Tool[];
22
22
  */
23
23
  export declare function loadGraphTools(): Promise<Tool[]>;
24
24
  export declare function handleLine(line: string, write?: (s: string) => void): void;
25
- export declare const MCP_USAGE = "usage: geml mcp --root <dir> [--graph <dir>] [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the\n read-only code-graph tools when the root holds a code graph.\n\n --root <dir> REQUIRED. Root directory holding the .geml documents.\n Relative paths resolve against the server process's CWD,\n which the CLIENT chooses \u2014 pass an absolute path.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --graph <dir> Code-graph directory, inside --root. Defaults to\n <root>/.geml-code-graph when that holds an index.geml.\n With no graph, the code-graph tools are not served\n at all (a client sees only the document tools).\n --no-history Do not auto-commit a .gemlhistory revision before each\n write. Default is to commit, so geml_revert always\n has a revision to undo to.\n\n Register with a client:\n claude mcp add geml -- geml mcp --root /abs/path/to/repo";
25
+ export declare const MCP_USAGE = "usage: geml mcp --root <dir> [--graph <dir>] [--no-history]\n\n Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the\n read-only code-graph tools when the root holds a code graph.\n\n --root <dir> REQUIRED. Root directory holding the .geml documents.\n Relative paths resolve against the server process's CWD,\n which the CLIENT chooses \u2014 pass an absolute path.\n Every path a client names is confined to this directory;\n a client cannot widen or override it.\n --graph <dir> Code-graph directory, inside --root. Defaults to\n <root>/.geml-code-graph when that holds an index.geml.\n With no graph, the code-graph tools are not served\n at all (a client sees only the document tools).\n --no-history Do not save a .gemlhistory revision before each\n write. Default is to save one, so geml_revert always\n has a revision to undo to.\n\n Register with a client:\n claude mcp add geml -- geml mcp --root /abs/path/to/repo";
26
26
  export declare function parseArgs(args: string[]): McpOptions;
package/dist/mcp.js CHANGED
@@ -35,14 +35,14 @@
35
35
  // The mutations run through the CLI rather than re-implementing block editing:
36
36
  // the tool table is *defined* as CLI equivalences, and `-o -` already yields
37
37
  // the mutated document without touching the file — exactly the "produce, then
38
- // validate, then commit" order invariant 1 needs.
38
+ // validate, then save" order invariant 1 needs.
39
39
  import { readFileSync, writeFileSync, existsSync, realpathSync, statSync } from "node:fs";
40
40
  import { resolve, dirname, sep } from "node:path";
41
41
  import { fileURLToPath, pathToFileURL } from "node:url";
42
42
  import { spawnSync } from "node:child_process";
43
43
  import { createInterface } from "node:readline";
44
44
  import { parse, PARSER_VERSION } from "./geml.js";
45
- import { commit, listRevisions, isCurrent } from "./history.js";
45
+ import { save, listRevisions, isCurrent, resolveContent } from "./history.js";
46
46
  // One version for the whole package: `geml --version` and the MCP handshake
47
47
  // must not disagree. This used to be its own literal and had drifted to 0.1.0
48
48
  // against a 1.4.x package — invisible to everyone except the user reading their
@@ -200,12 +200,12 @@ function applyWrite(spec) {
200
200
  if (after === before) {
201
201
  return { ok: true, file: spec.file, diagnostics: diags, hint: "No change: the document already had this content." };
202
202
  }
203
- // 3. Commit the PRE-write state so this edit is revertible, then write.
203
+ // 3. Save the PRE-write state so this edit is revertible, then write.
204
204
  const revision = spec.summary && OPTS.history ? snapshot(real, spec.summary) : undefined;
205
205
  writeFileSync(real, after, "utf8");
206
206
  return { ok: true, file: spec.file, diagnostics: diags, revision };
207
207
  }
208
- // Commit the file's CURRENT bytes as a revision, so the about-to-happen write
208
+ // Save the file's CURRENT bytes as a revision, so the about-to-happen write
209
209
  // has something to revert to. A file already identical to its tip needs no
210
210
  // second revision.
211
211
  function snapshot(realPath, summary) {
@@ -213,7 +213,7 @@ function snapshot(realPath, summary) {
213
213
  try {
214
214
  if (existsSync(historyPath) && isCurrent(historyPath, realPath))
215
215
  return undefined;
216
- return commit({ gemlPath: realPath, historyPath, summary }).id;
216
+ return save({ gemlPath: realPath, historyPath, summary }).id;
217
217
  }
218
218
  catch {
219
219
  // A sidecar that cannot be written must not cost the caller their edit;
@@ -243,12 +243,27 @@ function docResolver(root, fromFile) {
243
243
  };
244
244
  }
245
245
  const hashId = (id) => (id.startsWith("#") ? id : `#${id}`);
246
+ // `geml get`/`geml set` take a full block SELECTOR, not only an id: a content
247
+ // address reaches a block the author never named, which is the whole point of
248
+ // `geml_list` now reporting one for those. So a value that is ALREADY a
249
+ // selector must pass through untouched — hashId would turn `@a3f9c1d2` into
250
+ // `#@a3f9c1d2` and address nothing. A bare word is still an id, so the
251
+ // long-standing "id with or without #" contract is unchanged.
252
+ //
253
+ // The parameter is still NAMED `id`: renaming it to `selector` would break
254
+ // every registered client for a cosmetic gain, and both design docs park that
255
+ // rename as a follow-up. The other verbs keep hashId — their CLI counterparts
256
+ // (add/delete/rename/revert) take ids only, so accepting a selector here would
257
+ // promise something the CLI would then refuse.
258
+ // A selector starts with `#` (id or heading line), `@` (content address), or a
259
+ // `=` fence run (type filter). Anything else is a bare id.
260
+ const selectorArg = (s) => (/^([#@]|={3,})/.test(s.trim()) ? s.trim() : `#${s}`);
246
261
  const FILE_ARG = { type: "string", description: "Document path relative to the server's --root directory, e.g. notes/spec.geml" };
247
262
  export const TOOLS = [
248
263
  // ----- read -----
249
264
  {
250
265
  name: "geml_list",
251
- description: "List every addressable block in a GEML document: its `#id`, kind, and heading text. Call this FIRST — the ids it returns are what every other tool in this server addresses. Cheaper and more reliable than reading the file to find out what is in it.",
266
+ description: "List every addressable block in a GEML document: its address, kind, and heading text. Call this FIRST — the `id` values it returns are what every other tool in this server addresses. Cheaper and more reliable than reading the file to find out what is in it. Rows marked `anon` have no `#id` (their `address` is a type or content address the CLI understands); this server's other tools take an `id`, so give such a block an id before addressing it here.",
252
267
  inputSchema: { type: "object", properties: { file: FILE_ARG }, required: ["file"] },
253
268
  run: (args) => {
254
269
  const real = resolveInRoot(args.file);
@@ -260,20 +275,24 @@ export const TOOLS = [
260
275
  },
261
276
  {
262
277
  name: "geml_get",
263
- description: "Read ONE block from a GEML document by its `#id`. Use this instead of reading the whole file: it returns only that block, typically a few percent of the document. Get available ids from `geml_list` first. Reading the whole file to change one block wastes context and risks modifying unrelated content.",
278
+ description: "Read ONE block from a GEML document. Use this instead of reading the whole file: it returns only that block, typically a few percent of the document. Call `geml_list` first and pass back the `address` it gives that also reaches blocks with no `#id`, which an id alone cannot.",
264
279
  inputSchema: {
265
280
  type: "object",
266
281
  properties: {
267
282
  file: FILE_ARG,
268
- id: { type: "string", description: "Block id, with or without the leading `#`" },
283
+ id: {
284
+ type: "string",
285
+ description: "What to read: a block id (with or without `#`), a `## Heading` line (its whole section), `=== type` for every block of a type, or a `@<hex>` content address for a block with no id — the forms `geml_list` prints",
286
+ },
269
287
  },
270
288
  required: ["file", "id"],
271
289
  },
272
290
  run: (args) => {
273
291
  const real = resolveInRoot(args.file);
274
- const run = runCli(["get", real, hashId(args.id)]);
292
+ const sel = selectorArg(args.id);
293
+ const run = runCli(["get", real, sel]);
275
294
  if (!run.ok)
276
- throw new Error(run.stderr || `no block with id ${hashId(args.id)}`);
295
+ throw new Error(run.stderr || `nothing matches ${sel}`);
277
296
  return run.stdout;
278
297
  },
279
298
  },
@@ -304,14 +323,41 @@ export const TOOLS = [
304
323
  },
305
324
  {
306
325
  name: "geml_history",
307
- description: "List the recorded revisions of a document, newest first. Each entry's `offset` is the selector `geml_revert` takes as `rev` (-1 is the revision before the current one). Use this to find WHICH revision to revert a block to; an empty list means the document has no sidecar yet and nothing can be reverted.",
308
- inputSchema: { type: "object", properties: { file: FILE_ARG }, required: ["file"] },
326
+ // The name mirrors the CLI COMMAND PATH (`geml history`), not a verb: this
327
+ // group's only read verb is `get`, and it is the only one that belongs on a
328
+ // server an agent drives (`save` would insert hand-made revisions between
329
+ // the automatic pre-write ones, and `restore` rewrites a whole file where
330
+ // the agent already has block-level geml_revert). So there will be no second
331
+ // history tool to disambiguate from, and `_get` would be a suffix that
332
+ // distinguishes nothing — design §5.
333
+ description: "Read a document's recorded history. WITHOUT `rev`: list every revision, newest first — each entry's `offset` is the selector `geml_revert` takes as `rev` (-1 is the revision before the current one), and an empty list means the document has no sidecar yet and nothing can be reverted. WITH `rev`: the full text of that one revision, for reading what the document looked like then without restoring it.",
334
+ inputSchema: {
335
+ type: "object",
336
+ properties: {
337
+ file: FILE_ARG,
338
+ rev: { type: "string", description: "Revision selector — `0` for the current tip, `-N` for N revisions back, or a revision id from the list. Omit it to get the list instead of one revision's text." },
339
+ },
340
+ required: ["file"],
341
+ },
309
342
  run: (args) => {
310
343
  const real = resolveInRoot(args.file);
311
344
  const historyPath = real.replace(/\.geml$/, "") + ".gemlhistory";
312
- if (!existsSync(historyPath))
345
+ const rev = args.rev === undefined ? undefined : String(args.rev);
346
+ if (!existsSync(historyPath)) {
347
+ // Naming a revision of a document that has no history at all is an
348
+ // error, not an empty result: the caller asked for specific content.
349
+ // The LIST tier stays a plain empty answer — "nothing yet" is a real,
350
+ // useful state there.
351
+ if (rev !== undefined)
352
+ throw new Error(`no .gemlhistory sidecar for ${args.file} yet, so revision ${rev} does not exist — the first write through this server creates one`);
313
353
  return { file: args.file, revisions: [], note: "no .gemlhistory sidecar yet — the first write through this server creates one" };
314
- return { file: args.file, revisions: listRevisions(historyPath) };
354
+ }
355
+ if (rev === undefined)
356
+ return { file: args.file, revisions: listRevisions(historyPath) };
357
+ // resolveContent() is the CLI's own path for `geml history get <file>
358
+ // <rev>`, so one selector grammar answers on both surfaces.
359
+ const { id, text } = resolveContent(historyPath, rev);
360
+ return { file: args.file, id, text };
315
361
  },
316
362
  },
317
363
  {
@@ -362,12 +408,15 @@ export const TOOLS = [
362
408
  // ----- write -----
363
409
  {
364
410
  name: "geml_set",
365
- description: "Replace ONE block, addressed by `#id`, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. `part` selects whole block (default), just the head/fence line, or just the body.",
411
+ description: "Replace ONE block, leaving every other byte of the document untouched. Prefer this over rewriting a file. The replacement is VALIDATED BEFORE it is written: if it would break the document, nothing is written and you get the diagnostics back — re-read them and fix the body rather than retrying the same content. `part` selects whole block (default), just the head/fence line, or just the body. An address matching SEVERAL blocks is refused — this writes one block, so narrow it first.",
366
412
  inputSchema: {
367
413
  type: "object",
368
414
  properties: {
369
415
  file: FILE_ARG,
370
- id: { type: "string", description: "Block id to replace, with or without `#`" },
416
+ id: {
417
+ type: "string",
418
+ description: "Which block to replace: an id (with or without `#`), or a `@<hex>` content address from `geml_list` for a block with no id. Must match exactly one block",
419
+ },
371
420
  body: { type: "string", description: "The replacement text" },
372
421
  part: { type: "string", enum: ["whole", "head", "body"], description: "What to replace (default: whole)" },
373
422
  },
@@ -381,9 +430,9 @@ export const TOOLS = [
381
430
  const flag = part === "head" ? ["--head"] : part === "body" ? ["--body"] : [];
382
431
  return applyWrite({
383
432
  file: args.file,
384
- cliArgs: ["set", real, hashId(args.id), ...flag, "--in", "-", "-o", "-"],
433
+ cliArgs: ["set", real, selectorArg(args.id), ...flag, "--in", "-", "-o", "-"],
385
434
  input: args.body,
386
- summary: `mcp: before write to ${hashId(args.id)}`,
435
+ summary: `mcp: before write to ${selectorArg(args.id)}`,
387
436
  });
388
437
  },
389
438
  },
@@ -619,25 +668,25 @@ export function handleLine(line, write = (s) => process.stdout.write(s)) {
619
668
  // ---------------------------------------------------------------------------
620
669
  // Entry
621
670
  // ---------------------------------------------------------------------------
622
- export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
623
-
624
- Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
625
- read-only code-graph tools when the root holds a code graph.
626
-
627
- --root <dir> REQUIRED. Root directory holding the .geml documents.
628
- Relative paths resolve against the server process's CWD,
629
- which the CLIENT chooses — pass an absolute path.
630
- Every path a client names is confined to this directory;
631
- a client cannot widen or override it.
632
- --graph <dir> Code-graph directory, inside --root. Defaults to
633
- <root>/.geml-code-graph when that holds an index.geml.
634
- With no graph, the code-graph tools are not served
635
- at all (a client sees only the document tools).
636
- --no-history Do not auto-commit a .gemlhistory revision before each
637
- write. Default is to commit, so geml_revert always
638
- has a revision to undo to.
639
-
640
- Register with a client:
671
+ export const MCP_USAGE = `usage: geml mcp --root <dir> [--graph <dir>] [--no-history]
672
+
673
+ Serve GEML document CRUD over the MCP stdio transport (JSON-RPC 2.0), plus the
674
+ read-only code-graph tools when the root holds a code graph.
675
+
676
+ --root <dir> REQUIRED. Root directory holding the .geml documents.
677
+ Relative paths resolve against the server process's CWD,
678
+ which the CLIENT chooses — pass an absolute path.
679
+ Every path a client names is confined to this directory;
680
+ a client cannot widen or override it.
681
+ --graph <dir> Code-graph directory, inside --root. Defaults to
682
+ <root>/.geml-code-graph when that holds an index.geml.
683
+ With no graph, the code-graph tools are not served
684
+ at all (a client sees only the document tools).
685
+ --no-history Do not save a .gemlhistory revision before each
686
+ write. Default is to save one, so geml_revert always
687
+ has a revision to undo to.
688
+
689
+ Register with a client:
641
690
  claude mcp add geml -- geml mcp --root /abs/path/to/repo`;
642
691
  export function parseArgs(args) {
643
692
  let root;
@@ -36,43 +36,43 @@ function page(title, body, ctx, source) {
36
36
  ? `<script type="importmap">{"imports":{"node:fs":"${lg}_node-stub.js","node:path":"${lg}_node-stub.js","node:crypto":"${lg}_node-stub.js","node:url":"${lg}_node-stub.js","node:child_process":"${lg}_node-stub.js"}}</script>\n`
37
37
  : "";
38
38
  const liveJs = wantLive
39
- ? `<script type="module">
40
- globalThis.process ??= { argv: [], env: {} };
41
- const { parse } = await import("${lg}geml.js");
42
- const { codeGraphWaves } = await import("${lg}render.js");
43
- const w = codeGraphWaves(async (rel) => {
44
- try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
45
- }, parse);
46
- for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
47
- const start = m.getAttribute("data-start");
48
- m._cgView = async (view) => {
49
- // A directed view builds from the node's OWN document (its meta names the
50
- // module and graph-depth); {doc} opens that document; else the mount's.
51
- const src = view && view.doc ? view.doc
52
- : view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
53
- : start;
54
- const r = await w.build(src, view && view.doc ? undefined : view);
55
- return r.error !== undefined ? null : r.data;
56
- };
57
- }
39
+ ? `<script type="module">
40
+ globalThis.process ??= { argv: [], env: {} };
41
+ const { parse } = await import("${lg}geml.js");
42
+ const { codeGraphWaves } = await import("${lg}render.js");
43
+ const w = codeGraphWaves(async (rel) => {
44
+ try { const r = await fetch(rel, { cache: "no-cache" }); return r.ok ? await r.text() : null; } catch { return null; }
45
+ }, parse);
46
+ for (const m of document.querySelectorAll(".cg-mount[data-start]")) {
47
+ const start = m.getAttribute("data-start");
48
+ m._cgView = async (view) => {
49
+ // A directed view builds from the node's OWN document (its meta names the
50
+ // module and graph-depth); {doc} opens that document; else the mount's.
51
+ const src = view && view.doc ? view.doc
52
+ : view && view.node ? view.node.slice(0, view.node.lastIndexOf("#"))
53
+ : start;
54
+ const r = await w.build(src, view && view.doc ? undefined : view);
55
+ return r.error !== undefined ? null : r.data;
56
+ };
57
+ }
58
58
  </script>\n`
59
59
  : "";
60
- return `<!doctype html>
61
- <html lang="en">
62
- <head>
63
- <meta charset="utf-8">
64
- <meta name="viewport" content="width=device-width, initial-scale=1">
65
- <title>${esc(title)}</title>
66
- <style>${CSS}</style>
67
- ${importMap}${mathHead}${mermaidHead}</head>
68
- <body>
69
- <main>
70
- ${body}
71
- </main>
72
- ${footer}
73
- <script>${JS}</script>
74
- ${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
75
- </html>
60
+ return `<!doctype html>
61
+ <html lang="en">
62
+ <head>
63
+ <meta charset="utf-8">
64
+ <meta name="viewport" content="width=device-width, initial-scale=1">
65
+ <title>${esc(title)}</title>
66
+ <style>${CSS}</style>
67
+ ${importMap}${mathHead}${mermaidHead}</head>
68
+ <body>
69
+ <main>
70
+ ${body}
71
+ </main>
72
+ ${footer}
73
+ <script>${JS}</script>
74
+ ${ctx.usedCodeGraph ? `<script>${CODE_GRAPH_JS}</script>\n` : ""}${liveJs}</body>
75
+ </html>
76
76
  `;
77
77
  }
78
78
  export function renderHtml(doc, opts = {}) {