@mmerterden/multi-agent-pipeline 16.12.0 → 16.14.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +4 -4
  3. package/README.tr.md +4 -4
  4. package/docs/adr/0010-own-code-graph.md +129 -0
  5. package/docs/adr/README.md +1 -0
  6. package/docs/architecture.md +2 -2
  7. package/docs/ecosystem.md +5 -5
  8. package/docs/features.md +8 -0
  9. package/package.json +1 -1
  10. package/pipeline/commands/multi-agent/graph/SKILL.md +105 -0
  11. package/pipeline/commands/multi-agent/help/SKILL.md +8 -8
  12. package/pipeline/commands/multi-agent/sync/SKILL.md +12 -12
  13. package/pipeline/commands/multi-agent/uninstall/SKILL.md +9 -7
  14. package/pipeline/multi-agent-refs/analysis/evidence.md +21 -0
  15. package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -10
  16. package/pipeline/multi-agent-refs/features/code-graph.md +69 -0
  17. package/pipeline/multi-agent-refs/features/model-fallback.md +44 -2
  18. package/pipeline/multi-agent-refs/knowledge.md +6 -0
  19. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  20. package/pipeline/multi-agent-refs/phases/phase-1-analysis.md +5 -0
  21. package/pipeline/multi-agent-refs/phases/phase-4-review.md +3 -3
  22. package/pipeline/multi-agent-refs/phases/phase-7-report.md +2 -0
  23. package/pipeline/preferences-template.json +2 -0
  24. package/pipeline/schemas/code-graph.schema.json +91 -0
  25. package/pipeline/schemas/prefs.schema.json +45 -0
  26. package/pipeline/schemas/token-budget.json +2 -2
  27. package/pipeline/scripts/_code-graph.mjs +545 -0
  28. package/pipeline/scripts/_path-match.mjs +87 -0
  29. package/pipeline/scripts/code-graph-rules/android.json +130 -0
  30. package/pipeline/scripts/code-graph-rules/go.json +124 -0
  31. package/pipeline/scripts/code-graph-rules/ios.json +95 -0
  32. package/pipeline/scripts/code-graph-rules/node.json +151 -0
  33. package/pipeline/scripts/code-graph-rules/python.json +91 -0
  34. package/pipeline/scripts/graph-affected.mjs +161 -0
  35. package/pipeline/scripts/graph-build.mjs +157 -0
  36. package/pipeline/scripts/graph-query.mjs +191 -0
  37. package/pipeline/scripts/graph-report.mjs +237 -0
  38. package/pipeline/scripts/smoke-cross-cli-behavior.sh +7 -4
  39. package/pipeline/scripts/test-gap-rules/go.json +24 -0
  40. package/pipeline/scripts/test-gap-rules/ios.json +38 -10
  41. package/pipeline/scripts/test-gap-scan.mjs +2 -21
  42. package/pipeline/scripts/uninstall.mjs +11 -2
  43. package/pipeline/scripts/validate-code-graph.mjs +174 -0
  44. package/pipeline/skills/.skills-index.json +14 -3
  45. package/pipeline/skills/shared/README.md +6 -5
  46. package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +106 -0
  47. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +11 -11
  48. package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +4 -4
  49. package/pipeline/skills/skills-index.md +4 -3
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file graph-affected.mjs - reverse traversal: what breaks if this changes.
5
+ *
6
+ * Phase 1's impact surface, feeding `analysis.touchedAreas[]`. Where
7
+ * graph-query.mjs answers "where does this live", this answers "who depends on
8
+ * it": it walks edges backwards from a named symbol, so the result is the blast
9
+ * radius of a change rather than its neighbourhood.
10
+ *
11
+ * Inputs:
12
+ * "<symbol>" Symbol name, exact or case-insensitive. Required.
13
+ * --graph <path> Graph file. Default: ~/.claude/knowledge/<cwd name>/code-graph.json
14
+ * --depth N Reverse traversal depth. Default: 2
15
+ * --kind K Restrict to one edge kind (repeatable via comma)
16
+ * --json Emit JSON instead of text
17
+ *
18
+ * Exit codes:
19
+ * 0 - answered (possibly empty)
20
+ * 1 - graph missing or unreadable
21
+ * 64 - usage error
22
+ *
23
+ * @module pipeline/scripts/graph-affected
24
+ */
25
+
26
+ import { resolve } from "node:path";
27
+ import { parseFlags } from "./graph-build.mjs";
28
+ import { loadGraph, defaultGraphPath } from "./graph-query.mjs";
29
+
30
+ /**
31
+ * Nodes whose name matches, preferring an exact hit.
32
+ *
33
+ * A case-insensitive fallback keeps the command usable from a task description,
34
+ * where the symbol is often typed in prose casing.
35
+ *
36
+ * @param {object} graph
37
+ * @param {string} name
38
+ * @returns {object[]}
39
+ */
40
+ export function findByName(graph, name) {
41
+ const exact = graph.nodes.filter((n) => n.name === name);
42
+ if (exact.length) return exact;
43
+ const lower = name.toLowerCase();
44
+ return graph.nodes.filter((n) => (n.name || "").toLowerCase() === lower);
45
+ }
46
+
47
+ /**
48
+ * Incoming adjacency only.
49
+ *
50
+ * @param {object} graph
51
+ * @param {Set<string>|null} kinds
52
+ * @returns {Map<string, Array<{id: string, kind: string}>>}
53
+ */
54
+ export function incoming(graph, kinds = null) {
55
+ const adj = new Map();
56
+ for (const e of graph.edges) {
57
+ if (kinds && !kinds.has(e.kind)) continue;
58
+ if (!adj.has(e.to)) adj.set(e.to, []);
59
+ adj.get(e.to).push({ id: e.from, kind: e.kind });
60
+ }
61
+ return adj;
62
+ }
63
+
64
+ /**
65
+ * Walk backwards from each seed.
66
+ *
67
+ * @param {object} params
68
+ * @returns {Array<{id: string, dist: number, kind: string, node: object}>}
69
+ */
70
+ export function affected({ graph, seeds, depth, kinds }) {
71
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
72
+ const adj = incoming(graph, kinds);
73
+ const seen = new Set(seeds.map((n) => n.id));
74
+ const out = [];
75
+ const queue = seeds.map((n) => ({ id: n.id, dist: 0, kind: "seed" }));
76
+
77
+ while (queue.length) {
78
+ const cur = queue.shift();
79
+ if (cur.dist > 0) out.push({ ...cur, node: byId.get(cur.id) });
80
+ if (cur.dist >= depth) continue;
81
+ for (const dep of adj.get(cur.id) || []) {
82
+ if (seen.has(dep.id)) continue;
83
+ seen.add(dep.id);
84
+ queue.push({ id: dep.id, dist: cur.dist + 1, kind: dep.kind });
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ function main() {
91
+ const { flags, positional } = parseFlags(process.argv.slice(2));
92
+ const name = positional.join(" ").trim();
93
+ if (!name) {
94
+ process.stderr.write(
95
+ 'graph-affected: a symbol name is required, e.g. graph-affected "Flight"\n',
96
+ );
97
+ process.exit(64);
98
+ }
99
+
100
+ const path = flags.graph && flags.graph !== true ? resolve(flags.graph) : defaultGraphPath();
101
+ let graph;
102
+ try {
103
+ graph = loadGraph(path);
104
+ } catch (e) {
105
+ process.stderr.write(`graph-affected: ${e.message}\n`);
106
+ process.exit(1);
107
+ }
108
+
109
+ const depth = Number(flags.depth) > 0 ? Number(flags.depth) : 2;
110
+ const kinds = flags.kind && flags.kind !== true ? new Set(String(flags.kind).split(",")) : null;
111
+
112
+ const seeds = findByName(graph, name);
113
+ if (seeds.length === 0) {
114
+ if (flags.json === true) {
115
+ process.stdout.write(
116
+ JSON.stringify({ symbol: name, seeds: [], affected: [] }, null, 2) + "\n",
117
+ );
118
+ } else {
119
+ process.stdout.write(`no node named '${name}'\n`);
120
+ }
121
+ return;
122
+ }
123
+
124
+ const hits = affected({ graph, seeds, depth, kinds });
125
+
126
+ if (flags.json === true) {
127
+ process.stdout.write(
128
+ JSON.stringify(
129
+ {
130
+ symbol: name,
131
+ seeds: seeds.map((s) => s.id),
132
+ affected: hits.map((h) => ({
133
+ id: h.id,
134
+ dist: h.dist,
135
+ edgeKind: h.kind,
136
+ path: h.node?.path || null,
137
+ })),
138
+ },
139
+ null,
140
+ 2,
141
+ ) + "\n",
142
+ );
143
+ return;
144
+ }
145
+
146
+ process.stdout.write(`# affected by ${name}\n\n`);
147
+ for (const seed of seeds) {
148
+ process.stdout.write(`* ${seed.name} (${seed.symbolKind || seed.kind}) ${seed.path || "-"}\n`);
149
+ }
150
+ process.stdout.write("\n");
151
+ const files = new Set();
152
+ for (const hit of hits) {
153
+ if (hit.node?.path) files.add(hit.node.path);
154
+ process.stdout.write(
155
+ `${" ".repeat(hit.dist)}- ${hit.node?.name || hit.id} [${hit.kind}] ${hit.node?.path || "-"}\n`,
156
+ );
157
+ }
158
+ process.stdout.write(`\n${hits.length} dependents across ${files.size} files\n`);
159
+ }
160
+
161
+ if (process.argv[1] && process.argv[1].endsWith("graph-affected.mjs")) main();
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file graph-build.mjs - build the code graph for one repo.
5
+ *
6
+ * Full rebuild every time, by measurement rather than by principle: on the
7
+ * corporate iOS app (4,311 Swift sources, 14.3 MB) the whole pass takes under
8
+ * three seconds, so an incremental extraction path would add a cache file, a
9
+ * staleness bug surface and a second code path to test in exchange for nothing.
10
+ * The manifest is still written - Phase 1 uses it to report how far behind a
11
+ * graph is, not to rebuild a subset.
12
+ *
13
+ * Inputs:
14
+ * --root <path> Repo root. Default: cwd
15
+ * --stack <id> ios | android | node | python. Required.
16
+ * --out <path> Graph output. Default: ~/.claude/knowledge/<project>/code-graph.json
17
+ * --max-nodes N Refuse to write a graph larger than this. Default: 200000.
18
+ * Callers pass prefs.global.codeGraph.maxNodes. A tree that
19
+ * blows past it is vendored or generated source, and writing
20
+ * a several-hundred-megabyte file for it helps nobody, so
21
+ * the build fails loudly instead of succeeding quietly.
22
+ * --json Emit the summary as JSON instead of a line
23
+ *
24
+ * Output:
25
+ * Writes the graph, prints a one-line summary.
26
+ *
27
+ * Exit codes:
28
+ * 0 - built
29
+ * 1 - rule load, walk, write error, or graph past --max-nodes
30
+ * 64 - usage error
31
+ *
32
+ * @module pipeline/scripts/graph-build
33
+ */
34
+
35
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
36
+ import { execFileSync } from "node:child_process";
37
+ import { join, dirname, resolve, basename } from "node:path";
38
+ import { homedir } from "node:os";
39
+ import { fileURLToPath } from "node:url";
40
+ import {
41
+ loadRules,
42
+ listSourceFiles,
43
+ extractFile,
44
+ buildGraph,
45
+ diffManifest,
46
+ } from "./_code-graph.mjs";
47
+
48
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
49
+ const DEFAULT_MAX_NODES = 200000;
50
+
51
+ export function parseFlags(argv) {
52
+ const flags = {};
53
+ const positional = [];
54
+ for (let i = 0; i < argv.length; i++) {
55
+ const a = argv[i];
56
+ if (a.startsWith("--")) {
57
+ const k = a.slice(2);
58
+ const v = argv[i + 1];
59
+ if (v !== undefined && !v.startsWith("--")) {
60
+ flags[k] = v;
61
+ i++;
62
+ } else {
63
+ flags[k] = true;
64
+ }
65
+ } else {
66
+ positional.push(a);
67
+ }
68
+ }
69
+ return { flags, positional };
70
+ }
71
+
72
+ export function defaultGraphPath(root) {
73
+ return join(homedir(), ".claude", "knowledge", basename(root), "code-graph.json");
74
+ }
75
+
76
+ function headCommit(root) {
77
+ try {
78
+ return execFileSync("git", ["-C", root, "rev-parse", "HEAD"], {
79
+ encoding: "utf8",
80
+ stdio: ["ignore", "pipe", "ignore"],
81
+ }).trim();
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ function main() {
88
+ const { flags } = parseFlags(process.argv.slice(2));
89
+ const stack = flags.stack;
90
+ if (!stack || stack === true) {
91
+ process.stderr.write("graph-build: --stack <ios|android|node|python> required\n");
92
+ process.exit(64);
93
+ }
94
+
95
+ const root = resolve(flags.root || process.cwd());
96
+ if (!existsSync(root)) {
97
+ process.stderr.write(`graph-build: root not found: ${root}\n`);
98
+ process.exit(1);
99
+ }
100
+ const out = flags.out && flags.out !== true ? resolve(flags.out) : defaultGraphPath(root);
101
+
102
+ let rules;
103
+ try {
104
+ rules = loadRules(stack, SCRIPT_DIR);
105
+ } catch (e) {
106
+ process.stderr.write(`graph-build: ${e.message}\n`);
107
+ process.exit(1);
108
+ }
109
+
110
+ const startedAt = Date.now();
111
+ const files = listSourceFiles(root, rules);
112
+ const extracted = new Map();
113
+ for (const path of files) {
114
+ try {
115
+ extracted.set(path, extractFile(readFileSync(join(root, path), "utf8"), rules));
116
+ } catch {
117
+ /* unreadable file: skip, it simply does not reach the graph */
118
+ }
119
+ }
120
+
121
+ const { manifest } = diffManifest(root, [...extracted.keys()], {});
122
+ const graph = buildGraph({
123
+ root,
124
+ stack,
125
+ extracted,
126
+ rules,
127
+ manifest,
128
+ baseCommit: headCommit(root),
129
+ generatedAt: new Date().toISOString(),
130
+ });
131
+
132
+ const maxNodes = Number(flags["max-nodes"]) > 0 ? Number(flags["max-nodes"]) : DEFAULT_MAX_NODES;
133
+ if (graph.stats.nodes > maxNodes) {
134
+ process.stderr.write(
135
+ `graph-build: ${graph.stats.nodes} nodes exceeds --max-nodes ${maxNodes}; ` +
136
+ `nothing written. Narrow the tree with the stack rules' excludePathGlobs, ` +
137
+ `or raise prefs.global.codeGraph.maxNodes if this repo really is that large.\n`,
138
+ );
139
+ process.exit(1);
140
+ }
141
+
142
+ mkdirSync(dirname(out), { recursive: true });
143
+ writeFileSync(out, JSON.stringify(graph), "utf8");
144
+
145
+ const elapsedMs = Date.now() - startedAt;
146
+ const summary = { out, elapsedMs, ...graph.stats };
147
+ if (flags.json === true) {
148
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
149
+ } else {
150
+ process.stdout.write(
151
+ `graph-build: ${graph.stats.files} files, ${graph.stats.nodes} nodes, ` +
152
+ `${graph.stats.edges} edges in ${elapsedMs}ms -> ${out}\n`,
153
+ );
154
+ }
155
+ }
156
+
157
+ if (process.argv[1] && process.argv[1].endsWith("graph-build.mjs")) main();
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @file graph-query.mjs - token-budgeted traversal of the code graph.
5
+ *
6
+ * Phase 1's read surface. Ranks graph nodes against the task text with the
7
+ * pipeline's own BM25 (`_retrieval.mjs`, already gated by eval-recall.mjs),
8
+ * takes the best seeds, then walks their edges breadth-first until the token
9
+ * budget is spent. The budget is the point: an unbounded neighbourhood dump is
10
+ * the context-stuffing anti-pattern this is supposed to replace, so the walk
11
+ * stops on budget rather than on depth alone.
12
+ *
13
+ * Inputs:
14
+ * "<question>" Free text. Required.
15
+ * --graph <path> Graph file. Default: ~/.claude/knowledge/<cwd name>/code-graph.json
16
+ * --budget N Output token cap. Default: 2000
17
+ * --depth N Max BFS depth from a seed. Default: 2
18
+ * --seeds N How many ranked seeds to expand. Default: 8
19
+ * --json Emit JSON instead of text
20
+ *
21
+ * Exit codes:
22
+ * 0 - answered (possibly empty)
23
+ * 1 - graph missing or unreadable
24
+ * 64 - usage error
25
+ *
26
+ * @module pipeline/scripts/graph-query
27
+ */
28
+
29
+ import { readFileSync, existsSync } from "node:fs";
30
+ import { join, resolve, basename } from "node:path";
31
+ import { homedir } from "node:os";
32
+ import { bm25 } from "./_retrieval.mjs";
33
+ import { parseFlags } from "./graph-build.mjs";
34
+
35
+ const CHARS_PER_TOKEN = 4;
36
+
37
+ export function defaultGraphPath(cwd = process.cwd()) {
38
+ return join(homedir(), ".claude", "knowledge", basename(resolve(cwd)), "code-graph.json");
39
+ }
40
+
41
+ export function loadGraph(path) {
42
+ if (!existsSync(path)) throw new Error(`graph not found: ${path}`);
43
+ const graph = JSON.parse(readFileSync(path, "utf8"));
44
+ if (!Array.isArray(graph.nodes) || !Array.isArray(graph.edges)) {
45
+ throw new Error(`graph is malformed: ${path}`);
46
+ }
47
+ return graph;
48
+ }
49
+
50
+ /**
51
+ * Adjacency in both directions.
52
+ *
53
+ * Traversal has to cross an edge either way: a task naming a base class must
54
+ * reach its subclasses (incoming) as readily as its own file (outgoing).
55
+ *
56
+ * @param {object} graph
57
+ * @returns {Map<string, Array<{id: string, kind: string, dir: string}>>}
58
+ */
59
+ export function adjacency(graph) {
60
+ const adj = new Map();
61
+ const push = (from, to, kind, dir) => {
62
+ if (!adj.has(from)) adj.set(from, []);
63
+ adj.get(from).push({ id: to, kind, dir });
64
+ };
65
+ for (const e of graph.edges) {
66
+ push(e.from, e.to, e.kind, "out");
67
+ push(e.to, e.from, e.kind, "in");
68
+ }
69
+ return adj;
70
+ }
71
+
72
+ /**
73
+ * Rank nodes against the question.
74
+ *
75
+ * `name` outweighs `path` because a task names symbols far more often than
76
+ * directories, and `symbolKind` carries a little signal ("the FlightList view
77
+ * model") without letting the kind word alone win a match.
78
+ *
79
+ * @param {object} graph
80
+ * @param {string} question
81
+ * @returns {Array<{node: object, score: number}>} descending, zero scores dropped
82
+ */
83
+ export function rankNodes(graph, question) {
84
+ const docs = graph.nodes.map((n) => ({
85
+ name: n.name || "",
86
+ path: n.path || "",
87
+ symbolKind: n.symbolKind || "",
88
+ }));
89
+ const scored = bm25(question, docs, { fieldWeights: { name: 3, path: 1, symbolKind: 1 } });
90
+ return scored
91
+ .filter((s) => s.score > 0)
92
+ .sort((a, b) => b.score - a.score)
93
+ .map((s) => ({ node: graph.nodes[s.index], score: s.score }));
94
+ }
95
+
96
+ function renderNode(node) {
97
+ const where = node.path ? `${node.path}${node.line ? `:${node.line}` : ""}` : "-";
98
+ const kind = node.symbolKind || node.kind;
99
+ return `${node.name} (${kind}) ${where}`;
100
+ }
101
+
102
+ /**
103
+ * Seed, then breadth-first expand until the budget is spent.
104
+ *
105
+ * @param {object} params
106
+ * @returns {{seeds: object[], hits: object[], truncated: boolean, tokens: number}}
107
+ */
108
+ export function traverse({ graph, question, budget, depth, seedCount }) {
109
+ const ranked = rankNodes(graph, question);
110
+ const seeds = ranked.slice(0, seedCount).map((r) => r.node);
111
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
112
+ const adj = adjacency(graph);
113
+
114
+ const seen = new Set(seeds.map((n) => n.id));
115
+ const hits = [];
116
+ let tokens = 0;
117
+ let truncated = false;
118
+
119
+ const queue = seeds.map((n) => ({ node: n, dist: 0, via: null }));
120
+ while (queue.length) {
121
+ const { node, dist, via } = queue.shift();
122
+ const cost = Math.ceil(renderNode(node).length / CHARS_PER_TOKEN);
123
+ if (tokens + cost > budget) {
124
+ truncated = true;
125
+ break;
126
+ }
127
+ tokens += cost;
128
+ hits.push({ id: node.id, dist, via, line: renderNode(node), degree: node.degree });
129
+
130
+ if (dist >= depth) continue;
131
+ const neighbours = (adj.get(node.id) || [])
132
+ .slice()
133
+ .sort((a, b) => (byId.get(b.id)?.degree || 0) - (byId.get(a.id)?.degree || 0));
134
+ for (const n of neighbours) {
135
+ if (seen.has(n.id)) continue;
136
+ const target = byId.get(n.id);
137
+ if (!target) continue;
138
+ seen.add(n.id);
139
+ queue.push({ node: target, dist: dist + 1, via: `${n.kind}/${n.dir}` });
140
+ }
141
+ }
142
+
143
+ return { seeds, hits, truncated, tokens };
144
+ }
145
+
146
+ function main() {
147
+ const { flags, positional } = parseFlags(process.argv.slice(2));
148
+ const question = positional.join(" ").trim();
149
+ if (!question) {
150
+ process.stderr.write('graph-query: a question is required, e.g. graph-query "flight search"\n');
151
+ process.exit(64);
152
+ }
153
+
154
+ const path = flags.graph && flags.graph !== true ? resolve(flags.graph) : defaultGraphPath();
155
+ let graph;
156
+ try {
157
+ graph = loadGraph(path);
158
+ } catch (e) {
159
+ process.stderr.write(`graph-query: ${e.message}\n`);
160
+ process.exit(1);
161
+ }
162
+
163
+ const budget = Number(flags.budget) > 0 ? Number(flags.budget) : 2000;
164
+ const depth = Number(flags.depth) > 0 ? Number(flags.depth) : 2;
165
+ const seedCount = Number(flags.seeds) > 0 ? Number(flags.seeds) : 8;
166
+
167
+ const result = traverse({ graph, question, budget, depth, seedCount });
168
+
169
+ if (flags.json === true) {
170
+ process.stdout.write(JSON.stringify({ question, budget, ...result }, null, 2) + "\n");
171
+ return;
172
+ }
173
+
174
+ process.stdout.write(`# ${question}\n\n`);
175
+ if (result.hits.length === 0) {
176
+ process.stdout.write("no matching nodes\n");
177
+ return;
178
+ }
179
+ for (const hit of result.hits) {
180
+ const prefix = hit.dist === 0 ? "*" : " ".repeat(hit.dist) + "-";
181
+ const via = hit.via ? ` [${hit.via}]` : "";
182
+ process.stdout.write(`${prefix} ${hit.line}${via}\n`);
183
+ }
184
+ process.stdout.write(
185
+ `\n${result.hits.length} nodes, ~${result.tokens} tokens` +
186
+ (result.truncated ? " (budget reached)" : "") +
187
+ "\n",
188
+ );
189
+ }
190
+
191
+ if (process.argv[1] && process.argv[1].endsWith("graph-query.mjs")) main();