@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/codemap/verify.mjs
CHANGED
|
@@ -1,158 +1,158 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
-
//
|
|
4
|
-
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
-
// native references).
|
|
6
|
-
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
-
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
-
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
-
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
-
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
-
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
-
//
|
|
13
|
-
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
-
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
-
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
-
import { posix } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
|
|
20
|
-
const args = process.argv.slice(2);
|
|
21
|
-
const flagI = args.indexOf("--geml");
|
|
22
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
-
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
-
process.exit(2);
|
|
25
|
-
}
|
|
26
|
-
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
-
const rootDir = resolve(dir);
|
|
28
|
-
|
|
29
|
-
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
-
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
-
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
-
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
-
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
-
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
-
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
-
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
-
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
-
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
-
// CRT rules for embedded " / trailing \.
|
|
40
|
-
// q quotes only when it must; WHEN it does it defers to shq, so a program path
|
|
41
|
-
// ending in a backslash cannot escape our own closing quote and swallow the
|
|
42
|
-
// next token. One set of CRT rules, in one place.
|
|
43
|
-
const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s));
|
|
44
|
-
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
45
|
-
// A codemap document's `src=` routes are written relative to the indexed
|
|
46
|
-
// SOURCE root (`geml-parser/src/attrs.ts` from a document two levels down), so
|
|
47
|
-
// checking one has to be told that root — the same value `serve` derives, from
|
|
48
|
-
// the recorded recipe, falling back to the parent of the graph directory.
|
|
49
|
-
let srcRoot = resolve(rootDir, "..");
|
|
50
|
-
try { srcRoot = resolve(rootDir, JSON.parse(readFileSync(join(rootDir, "_index", "refresh.json"), "utf8")).root ?? ".."); } catch { /* no recipe: parent */ }
|
|
51
|
-
const checkArgs = (file) => ["check", "--root", srcRoot, file];
|
|
52
|
-
const runCheck = (file) => {
|
|
53
|
-
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
54
|
-
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" });
|
|
55
|
-
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
56
|
-
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
57
|
-
// string (never an args array — that is the unescaped, injection-prone path).
|
|
58
|
-
if (process.platform === "win32") {
|
|
59
|
-
return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
60
|
-
}
|
|
61
|
-
return spawnSync(cli, checkArgs(file), { encoding: "utf8" });
|
|
62
|
-
};
|
|
63
|
-
if (!existsSync(localParser)) {
|
|
64
|
-
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
68
|
-
|
|
69
|
-
const files = [];
|
|
70
|
-
const walk = (d) => {
|
|
71
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
72
|
-
const p = join(d, e.name);
|
|
73
|
-
if (e.isDirectory()) walk(p);
|
|
74
|
-
else if (e.name.endsWith(".geml")) files.push(p);
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
walk(rootDir);
|
|
78
|
-
files.sort();
|
|
79
|
-
|
|
80
|
-
// ---- pass 1: geml check ----
|
|
81
|
-
let failed = 0;
|
|
82
|
-
for (const f of files) {
|
|
83
|
-
const r = runCheck(f);
|
|
84
|
-
if (r.status !== 0) {
|
|
85
|
-
failed++;
|
|
86
|
-
console.error(`FAIL ${f}`);
|
|
87
|
-
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---- pass 2: codemap profile references ----
|
|
92
|
-
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
93
|
-
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
94
|
-
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
95
|
-
// function — tolerated, nothing to resolve).
|
|
96
|
-
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
97
|
-
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
98
|
-
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
99
|
-
const collectIds = (blocks, ids) => {
|
|
100
|
-
for (const b of blocks) {
|
|
101
|
-
if (b.id) ids.add(b.id);
|
|
102
|
-
if (b.children) collectIds(b.children, ids);
|
|
103
|
-
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
for (const f of files) {
|
|
107
|
-
const doc = parse(readFileSync(f, "utf8"));
|
|
108
|
-
const ids = new Set();
|
|
109
|
-
collectIds(doc.children, ids);
|
|
110
|
-
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
let refErrors = 0;
|
|
114
|
-
const err = (doc, where, msg) => {
|
|
115
|
-
refErrors++;
|
|
116
|
-
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
117
|
-
};
|
|
118
|
-
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
119
|
-
ref = String(ref).trim();
|
|
120
|
-
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
121
|
-
const h = ref.indexOf("#");
|
|
122
|
-
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
123
|
-
let targetDoc = fromDoc;
|
|
124
|
-
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
125
|
-
const id = ref.slice(h + 1);
|
|
126
|
-
const target = docs.get(targetDoc);
|
|
127
|
-
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
128
|
-
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
129
|
-
const bare = id.split(".")[0];
|
|
130
|
-
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
for (const [docPath, { blocks }] of docs) {
|
|
134
|
-
for (const b of blocks) {
|
|
135
|
-
if (b.kind !== "block") continue;
|
|
136
|
-
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
137
|
-
const lenient = LINK_TABLES.has(b.id);
|
|
138
|
-
const fromCol = b.table.columns.indexOf("from");
|
|
139
|
-
const toCol = b.table.columns.indexOf("to");
|
|
140
|
-
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
141
|
-
b.table.rows.forEach((row, i) => {
|
|
142
|
-
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
143
|
-
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
if (b.type === "meta" && b.data?.entry) {
|
|
147
|
-
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
148
|
-
checkRef(docPath, "meta entry", ref);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
console.error(
|
|
155
|
-
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
156
|
-
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
157
|
-
);
|
|
158
|
-
process.exit(failed || refErrors ? 1 : 0);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// geml-code-graph verify — the codemap's correctness oracle, two passes:
|
|
3
|
+
//
|
|
4
|
+
// 1. `geml check` over every .geml (document structure, id uniqueness,
|
|
5
|
+
// native references).
|
|
6
|
+
// 2. The codemap-profile pass (docs/codemap-profile.md): CSV cells and meta
|
|
7
|
+
// values are opaque to the GEML standard BY DESIGN (the standard stays
|
|
8
|
+
// untouched), so edge integrity is checked here — the from/to columns of
|
|
9
|
+
// #calls / #called-by / #ref-by tables and every meta `entry` value must
|
|
10
|
+
// resolve (`#id` in the same document, `doc.geml#id` in a sibling).
|
|
11
|
+
// A renamed or deleted method therefore fails the build, not the reader.
|
|
12
|
+
//
|
|
13
|
+
// geml codemap verify [dir] [--geml <path-to-geml.js|geml>]
|
|
14
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join, resolve, dirname, relative } from "node:path";
|
|
16
|
+
import { posix } from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { spawnSync } from "node:child_process";
|
|
19
|
+
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const flagI = args.indexOf("--geml");
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.error("usage: geml codemap verify [dir] [--geml <path>] (dir defaults to ./.geml-code-graph)");
|
|
24
|
+
process.exit(2);
|
|
25
|
+
}
|
|
26
|
+
const dir = args.find((a, i) => !a.startsWith("-") && (flagI < 0 || i !== flagI + 1)) || ".geml-code-graph";
|
|
27
|
+
const rootDir = resolve(dir);
|
|
28
|
+
|
|
29
|
+
// Resolve the geml CLI (pass 1) and the parser API (pass 2).
|
|
30
|
+
const localParser = resolve(dirname(fileURLToPath(import.meta.url)), "../dist/geml.js");
|
|
31
|
+
let cli = flagI >= 0 ? args[flagI + 1] : undefined;
|
|
32
|
+
if (!cli) cli = existsSync(localParser) ? localParser : "geml";
|
|
33
|
+
// win32 cmd.exe quoting (same rationale as build.mjs). q: space-aware quote for
|
|
34
|
+
// the PROGRAM token — a bare launcher name (geml resolved via PATH) whose
|
|
35
|
+
// .cmd/.bat shim uses %~dp0 breaks if the name is blanket-quoted. shq: ALWAYS
|
|
36
|
+
// double-quote ARGUMENTS — Node does not escape args under shell:true, so a
|
|
37
|
+
// `.geml` filename containing & | ( ) would otherwise break out and inject.
|
|
38
|
+
// cmd.exe treats those metacharacters and whitespace as literal inside quotes;
|
|
39
|
+
// CRT rules for embedded " / trailing \.
|
|
40
|
+
// q quotes only when it must; WHEN it does it defers to shq, so a program path
|
|
41
|
+
// ending in a backslash cannot escape our own closing quote and swallow the
|
|
42
|
+
// next token. One set of CRT rules, in one place.
|
|
43
|
+
const q = (s) => (/[\s"]/.test(String(s)) ? shq(s) : String(s));
|
|
44
|
+
const shq = (s) => `"${String(s).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/, '$1$1')}"`;
|
|
45
|
+
// A codemap document's `src=` routes are written relative to the indexed
|
|
46
|
+
// SOURCE root (`geml-parser/src/attrs.ts` from a document two levels down), so
|
|
47
|
+
// checking one has to be told that root — the same value `serve` derives, from
|
|
48
|
+
// the recorded recipe, falling back to the parent of the graph directory.
|
|
49
|
+
let srcRoot = resolve(rootDir, "..");
|
|
50
|
+
try { srcRoot = resolve(rootDir, JSON.parse(readFileSync(join(rootDir, "_index", "refresh.json"), "utf8")).root ?? ".."); } catch { /* no recipe: parent */ }
|
|
51
|
+
const checkArgs = (file) => ["check", "--root", srcRoot, file];
|
|
52
|
+
const runCheck = (file) => {
|
|
53
|
+
// The built-parser path: run node on geml.js directly (array args, no shell).
|
|
54
|
+
if (cli.endsWith(".js")) return spawnSync(process.execPath, [cli, ...checkArgs(file)], { encoding: "utf8" });
|
|
55
|
+
// A non-.js cli may be a .cmd/.bat launcher (e.g. geml.cmd on PATH), which
|
|
56
|
+
// Node can only spawn through the shell. Hand cmd.exe ONE pre-escaped command
|
|
57
|
+
// string (never an args array — that is the unescaped, injection-prone path).
|
|
58
|
+
if (process.platform === "win32") {
|
|
59
|
+
return spawnSync([q(cli), ...checkArgs(file).map(shq)].join(" "), { encoding: "utf8", shell: true });
|
|
60
|
+
}
|
|
61
|
+
return spawnSync(cli, checkArgs(file), { encoding: "utf8" });
|
|
62
|
+
};
|
|
63
|
+
if (!existsSync(localParser)) {
|
|
64
|
+
console.error("verify: the profile pass needs the built parser (cd geml-parser && npm install && npm run build)");
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const { parse } = await import(`file://${localParser.replace(/\\/g, "/")}`);
|
|
68
|
+
|
|
69
|
+
const files = [];
|
|
70
|
+
const walk = (d) => {
|
|
71
|
+
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
72
|
+
const p = join(d, e.name);
|
|
73
|
+
if (e.isDirectory()) walk(p);
|
|
74
|
+
else if (e.name.endsWith(".geml")) files.push(p);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
walk(rootDir);
|
|
78
|
+
files.sort();
|
|
79
|
+
|
|
80
|
+
// ---- pass 1: geml check ----
|
|
81
|
+
let failed = 0;
|
|
82
|
+
for (const f of files) {
|
|
83
|
+
const r = runCheck(f);
|
|
84
|
+
if (r.status !== 0) {
|
|
85
|
+
failed++;
|
|
86
|
+
console.error(`FAIL ${f}`);
|
|
87
|
+
console.error((r.stderr || r.stdout || "").split("\n").slice(0, 4).map((l) => ` ${l}`).join("\n"));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- pass 2: codemap profile references ----
|
|
92
|
+
const REF_TABLES = new Set(["calls", "called-by", "ref-by"]);
|
|
93
|
+
// Cross-stack link tables: `from`/`to` may be a #ref (resolved cross-tree
|
|
94
|
+
// link — checked) OR plain `file:line` text (a call/route outside any indexed
|
|
95
|
+
// function — tolerated, nothing to resolve).
|
|
96
|
+
const LINK_TABLES = new Set(["api-calls", "api-served-by"]);
|
|
97
|
+
const relDoc = (f) => relative(rootDir, f).replace(/\\/g, "/");
|
|
98
|
+
const docs = new Map(); // relPath -> { ids:Set, blocks }
|
|
99
|
+
const collectIds = (blocks, ids) => {
|
|
100
|
+
for (const b of blocks) {
|
|
101
|
+
if (b.id) ids.add(b.id);
|
|
102
|
+
if (b.children) collectIds(b.children, ids);
|
|
103
|
+
if (b.items) for (const it of b.items) if (it.children) collectIds(it.children, ids);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
for (const f of files) {
|
|
107
|
+
const doc = parse(readFileSync(f, "utf8"));
|
|
108
|
+
const ids = new Set();
|
|
109
|
+
collectIds(doc.children, ids);
|
|
110
|
+
docs.set(relDoc(f), { ids, blocks: doc.children });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let refErrors = 0;
|
|
114
|
+
const err = (doc, where, msg) => {
|
|
115
|
+
refErrors++;
|
|
116
|
+
console.error(`REF ${doc} ${where}: ${msg}`);
|
|
117
|
+
};
|
|
118
|
+
const checkRef = (fromDoc, where, ref, lenient = false) => {
|
|
119
|
+
ref = String(ref).trim();
|
|
120
|
+
if (!ref) return lenient ? undefined : err(fromDoc, where, "empty reference cell");
|
|
121
|
+
const h = ref.indexOf("#");
|
|
122
|
+
if (h < 0) return lenient ? undefined : err(fromDoc, where, `not a reference: \`${ref}\``);
|
|
123
|
+
let targetDoc = fromDoc;
|
|
124
|
+
if (h > 0) targetDoc = posix.normalize(posix.join(posix.dirname(fromDoc), ref.slice(0, h)));
|
|
125
|
+
const id = ref.slice(h + 1);
|
|
126
|
+
const target = docs.get(targetDoc);
|
|
127
|
+
if (!target) return err(fromDoc, where, `cannot resolve document \`${ref.slice(0, h)}\``);
|
|
128
|
+
// reads/writes values may carry a plain-text `.member` suffix (ids never contain '.')
|
|
129
|
+
const bare = id.split(".")[0];
|
|
130
|
+
if (!target.ids.has(bare)) return err(fromDoc, where, `unresolved reference \`${ref}\``);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const [docPath, { blocks }] of docs) {
|
|
134
|
+
for (const b of blocks) {
|
|
135
|
+
if (b.kind !== "block") continue;
|
|
136
|
+
if (b.type === "table" && (REF_TABLES.has(b.id) || LINK_TABLES.has(b.id)) && b.table) {
|
|
137
|
+
const lenient = LINK_TABLES.has(b.id);
|
|
138
|
+
const fromCol = b.table.columns.indexOf("from");
|
|
139
|
+
const toCol = b.table.columns.indexOf("to");
|
|
140
|
+
if (fromCol < 0 || toCol < 0) { err(docPath, `#${b.id}`, "missing from/to columns"); continue; }
|
|
141
|
+
b.table.rows.forEach((row, i) => {
|
|
142
|
+
checkRef(docPath, `#${b.id} row ${i + 1} from`, row[fromCol]?.text ?? "", lenient);
|
|
143
|
+
checkRef(docPath, `#${b.id} row ${i + 1} to`, row[toCol]?.text ?? "", lenient);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (b.type === "meta" && b.data?.entry) {
|
|
147
|
+
for (const ref of String(b.data.entry).split(/\s+/).filter(Boolean)) {
|
|
148
|
+
checkRef(docPath, "meta entry", ref);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
console.error(
|
|
155
|
+
`verify: ${files.length - failed}/${files.length} documents pass geml check; `
|
|
156
|
+
+ `profile references: ${refErrors === 0 ? "all resolve" : `${refErrors} dangling`}`,
|
|
157
|
+
);
|
|
158
|
+
process.exit(failed || refErrors ? 1 : 0);
|
package/dist/cli.js
CHANGED
|
@@ -125,78 +125,78 @@ 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
|
-
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.
|
|
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.
|
|
200
200
|
`;
|
|
201
201
|
// One-line usage for each subcommand — the single source for both the error
|
|
202
202
|
// shown on misuse and the `<cmd> --help` text.
|
|
@@ -211,65 +211,65 @@ const SUBHELP = {
|
|
|
211
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)",
|
|
212
212
|
check: "usage: geml check <file.geml|-> [--root <dir>] [--json] (--root: resolve cross-doc refs within <dir> instead of the file's own directory)",
|
|
213
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
|
+
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.
|
|
219
219
|
All four take --history <path> to point at a sidecar other than <file>.gemlhistory.)`,
|
|
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)
|
|
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)
|
|
227
227
|
(<dir> for verify/render/serve/refresh/find defaults to ./.geml-code-graph; codegraph and code-graph are accepted as aliases of codemap)`,
|
|
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:
|
|
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:
|
|
251
251
|
claude mcp add geml -- geml mcp --root /abs/path/to/repo`,
|
|
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
|
|
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
|
|
273
273
|
skipped and named; no tool directory is ever created for you.`,
|
|
274
274
|
};
|
|
275
275
|
// Set from argv at dispatch time; when true, errors are emitted as a JSON
|
|
@@ -2292,7 +2292,7 @@ function runCodemap(args) {
|
|
|
2292
2292
|
// `unknown codemap subcommand`: this string is what an operator sees in a
|
|
2293
2293
|
// client's server log when the entry they registered stops starting.
|
|
2294
2294
|
if (sub === "mcp") {
|
|
2295
|
-
fail("geml codemap mcp was removed: use `geml mcp --root <dir>`, which serves the
|
|
2295
|
+
fail("geml codemap mcp was removed: use `geml mcp --root <dir>`, which serves the four code-graph tools alongside the document tools (graph: <root>/.geml-code-graph, or --graph <dir>).");
|
|
2296
2296
|
}
|
|
2297
2297
|
const script = scripts[sub];
|
|
2298
2298
|
if (!script)
|