@m6d/cortex-cli 1.0.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/README.md ADDED
@@ -0,0 +1,135 @@
1
+ # @m6d/cortex-cli
2
+
3
+ The `cortex` command line: scaffold Cortex servers, and run the generators that otherwise need a
4
+ hand-written script.
5
+
6
+ ```sh
7
+ bunx @m6d/cortex-cli new my-app # from nowhere — scaffolds a project
8
+ bunx cortex swagger sync # inside a project — reads its cortex.config.ts
9
+ bunx cortex graph seed
10
+ ```
11
+
12
+ Commands are noun-first — `cortex <resource> <verb>` — except `new`, which acts on no existing
13
+ resource. Run `cortex --help`, then `cortex <resource> --help`.
14
+
15
+ ```sh
16
+ bun add -d @m6d/cortex-cli
17
+ ```
18
+
19
+ Requires [Bun](https://bun.sh). The binary re-execs itself under Bun when Node started it, and tells
20
+ you how to install Bun when it is genuinely absent.
21
+
22
+ ## `cortex new`
23
+
24
+ Scaffolds `./<name>`: a standalone project with `cortex.config.ts`, `index.ts`, `.env` and
25
+ `.env.example`, inlined eslint/prettier/cspell/fallow config, and a `docker-compose.yml` when a
26
+ selection contributes a service. It then runs `bun install`, and `git init` unless the target is
27
+ already inside a work tree. An existing non-empty directory is an error.
28
+
29
+ ```sh
30
+ bunx @m6d/cortex-cli new widget-backend --knowledge --redis
31
+ ```
32
+
33
+ On a TTY it asks only what the flags left open — the name, the database, and one multi-select over
34
+ the undecided features. With no TTY it never prompts and behaves as `--yes`, so a missing name is a
35
+ hard error rather than a hang.
36
+
37
+ | Flag | Default | Writes |
38
+ | ------------------------------------------ | ---------- | ------------------------------------------------------------------------- |
39
+ | `--database <postgres\|mssql>` | `postgres` | The `database` block. `postgres` also gets a compose service; `mssql` not |
40
+ | `--attachments` / `--no-attachments` | off | `storage` + `vision` — MinIO uploads, described by a vision model |
41
+ | `--redis` / `--no-redis` | off | `redis` — resumable streams across restarts |
42
+ | `--auth` / `--no-auth` | off | `auth` — JWT verification via JWKS |
43
+ | `--knowledge` / `--no-knowledge` | off | `neo4j` + `embedding` + `knowledge`, plus a `src/domains/` tree |
44
+ | `--control-center` / `--no-control-center` | off | `controlCenter` |
45
+ | `--yes` | | Accept the defaults instead of asking |
46
+
47
+ Unselected features are still written to `cortex.config.ts`, commented out, alongside `fastModel`,
48
+ `reranker`, and `context` — the generated config documents what else exists. Every credential and
49
+ URL is a placeholder in `.env`; none of them is prompted for.
50
+
51
+ ## `cortex swagger sync` / `check`
52
+
53
+ Fetches every Swagger URL the config names — server-level and per-agent, deduped — and reconciles
54
+ the `.endpoint.ts` files under the domains directory against it.
55
+
56
+ ```sh
57
+ bunx cortex swagger sync
58
+ bunx cortex swagger check
59
+ bunx cortex swagger sync --domains-dir ./server/src/domains
60
+ ```
61
+
62
+ `sync` rewrites each endpoint's params, body, and response schemas in place and prints only the
63
+ files it changed; unchanged ones live in the footer count. `check` writes nothing, reports the drift
64
+ it found, and exits 2 — which is what makes it a CI step.
65
+
66
+ An endpoint file pointing at a route the spec no longer serves is `MISSING`. `sync` prints those
67
+ last, so they are the thing you read, and still exits 0: writing is its job. `check` counts them as
68
+ a finding, same as drift.
69
+
70
+ `-d, --domains-dir <path>` is declared on the `swagger` group, so it works on either side of the
71
+ verb. It defaults to `src/domains`, resolved relative to the config file.
72
+
73
+ ## `cortex graph seed`
74
+
75
+ Loads every domain the config declares — server-level `knowledge.domains` and per-agent ones,
76
+ deduped — and seeds Neo4j with the concepts, endpoints, services, and rules they declare, then
77
+ embeds them. Reports per domain, because a domain is the unit you edit. No
78
+ flags.
79
+
80
+ ```sh
81
+ bunx cortex graph seed
82
+ ```
83
+
84
+ Seeding is `MERGE`-based, so re-running is safe. If any statement failed, the graph is partially
85
+ seeded and the command exits 1 — a half-seeded graph should not pass as success.
86
+
87
+ ## Global options
88
+
89
+ | Option | Description |
90
+ | --------------------- | ----------------------------------------------------- |
91
+ | `-c, --config <path>` | Path to the config file. Default `./cortex.config.ts` |
92
+ | `-v, --verbose` | Print full errors and the commands being run |
93
+ | `-V, --version` | Output the version number |
94
+ | `-h, --help` | Display help for command |
95
+
96
+ Config discovery is that exact filename in the directory you run in, with no upward search. There is
97
+ no `--cwd`: Bun auto-loads `.env` relative to the process cwd before any of our code runs, so
98
+ reading another directory's config from here would pair it with the wrong environment. `cd` first,
99
+ or point `--config` at the file.
100
+
101
+ The config is inert data — a plain object literal the CLI imports. Any key that is present but
102
+ evaluates to `undefined`, usually a missing environment variable, is listed by path and stops the
103
+ run.
104
+
105
+ `graph seed` runs standalone: it never constructs or serves your `@m6d/cortex-server` — it only
106
+ imports your `cortex.config.ts` (whose own imports may pull the server's modules in, inertly).
107
+ Its Neo4j client, its embedder and its `GRAPH_SCHEMA` come from the graph contract the server
108
+ reads the graph back with (each package vendors its own copy at pack time; the server re-exports
109
+ it) — one contract, shared from both ends. Seeding stamps the schema
110
+ version it wrote onto the graph; if that graph already carries a different one, the CLI says so and
111
+ seeds anyway, noting that nodes from the old schema are not removed. Skew is caught on the reading
112
+ side, where it matters: the server checks the stamp on its first resolve and refuses a graph it
113
+ cannot read.
114
+
115
+ `swagger` writes `.endpoint.ts` source whose generated block has to satisfy the server's own
116
+ `EndpointDef`, so a codegen that outran the installed version is a loud type error in your
117
+ `bun run check`.
118
+
119
+ Your `cortex.config.ts` imports `@m6d/cortex-server` for `defineAgent`, so a project without it
120
+ installed fails on the import — reported as an unloadable config, with the missing package named.
121
+
122
+ ## Exit codes
123
+
124
+ | Command | 0 | 1 | 2 |
125
+ | --------------- | --------------------------------------------- | ----------------------------------- | ---------------------- |
126
+ | `new` | Files written — even if `bun install` failed | Bad target, missing name, cancelled | — |
127
+ | `swagger sync` | Wrote what it could, `MISSING` files included | Couldn't run | — |
128
+ | `swagger check` | Every endpoint file matched and in sync | Couldn't run | Drift and/or `MISSING` |
129
+ | `graph seed` | Everything seeded | A statement failed, or couldn't run | — |
130
+
131
+ _Couldn't run_ means no config file, an unloadable config (a missing `@m6d/cortex-server` in the
132
+ project reads as one), undefined values in it, an absent domains directory, or an unreachable
133
+ Swagger host or Neo4j.
134
+
135
+ Design: [`docs/design/cortex-cli.md`](../../docs/design/cortex-cli.md).
package/bin/cortex.js ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Plain JavaScript, and a Node shebang, on purpose: `bunx` honours the shebang
4
+ // unless you pass `--bun`, so Node parses this file on the normal path. A
5
+ // `#!/usr/bin/env bun` shebang would give a user without Bun
6
+ // `env: bun: No such file or directory` — unrecoverable, because none of our
7
+ // code ever runs. See docs/design/cortex-cli.md §4.
8
+
9
+ import { spawnSync } from "node:child_process";
10
+ import { fileURLToPath } from "node:url";
11
+
12
+ if (process.versions.bun) {
13
+ await import("../src/cli");
14
+ } else {
15
+ const cli = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
16
+ const args = [cli, ...process.argv.slice(2)];
17
+ let result = spawnSync("bun", args, { stdio: "inherit" });
18
+
19
+ // Node's CVE-2024-27980 fix refuses to spawn a `.cmd` file directly, which is
20
+ // exactly what an npm-installed Bun is on Windows: the user has Bun and still
21
+ // gets EINVAL. `shell: true` is the documented way through; every argument is
22
+ // quoted because cmd.exe splits on the spaces in a Windows path otherwise.
23
+ if (result.error?.code === "EINVAL" && process.platform === "win32") {
24
+ result = spawnSync("bun", args.map(quote), { stdio: "inherit", shell: true });
25
+ }
26
+
27
+ // Any spawn error at all means Bun did not run, and the reason matters less
28
+ // than the fix — a raw `Error: spawnSync bun EACCES` helps nobody.
29
+ if (result.error) {
30
+ process.stderr.write(
31
+ "\n■ cortex needs Bun.\n" +
32
+ (result.error.code === "ENOENT" ? "" : `\n ${result.error.message}\n`) +
33
+ "\n curl -fsSL https://bun.sh/install | bash\n" +
34
+ "\n Then: bunx @m6d/cortex-cli new\n\n",
35
+ );
36
+ process.exit(1);
37
+ }
38
+
39
+ // A signal-killed child reports `status: null`. Exiting 1 would turn the
40
+ // user's Ctrl-C into a generic failure, so the signal is re-raised on
41
+ // ourselves and the shell sees the 130/143 it expects. If the signal is
42
+ // ignored or blocked and we survive it, the exit below still runs.
43
+ if (result.signal) {
44
+ process.kill(process.pid, result.signal);
45
+ }
46
+
47
+ process.exit(result.status ?? 1);
48
+ }
49
+
50
+ function quote(argument) {
51
+ return `"${argument}"`;
52
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@m6d/cortex-cli",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold and operate Cortex servers",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "cortex": "./bin/cortex.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src",
13
+ "!src/**/*.test.ts",
14
+ "tsconfig.json",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "check": "tsc --noEmit",
19
+ "prepack": "cp -R ../../internal/contracts src/contracts && rm src/contracts/package.json src/contracts/tsconfig.json",
20
+ "postpack": "rm -rf src/contracts",
21
+ "test": "bun test"
22
+ },
23
+ "dependencies": {
24
+ "@clack/prompts": "^1.7.0",
25
+ "@commander-js/extra-typings": "^15.0.0",
26
+ "commander": "^15.0.0",
27
+ "picocolors": "^1.1.1"
28
+ },
29
+ "devDependencies": {
30
+ "@cortex/contracts": "workspace:*",
31
+ "@types/bun": "latest",
32
+ "typescript": "~5.9.3"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { Command, Help, Option } from "@commander-js/extra-typings";
2
+
3
+ import pkg from "../package.json";
4
+ import { registerGraph } from "@/commands/graph";
5
+ import { registerNew } from "@/commands/new";
6
+ import { registerSwagger } from "@/commands/swagger";
7
+ import * as report from "@/ui/report";
8
+
9
+ // Discovery is this exact name in the cwd, with no upward walk (design spec §6.2),
10
+ // so the flag's default *is* the discovery rule. The second argument is what help
11
+ // prints — without it commander would JSON.stringify the path and show quotes.
12
+ const DEFAULT_CONFIG = "./cortex.config.ts";
13
+
14
+ const program = new Command()
15
+ .name("cortex")
16
+ .description("Scaffold and operate Cortex servers.")
17
+ .addOption(
18
+ new Option("-c, --config <path>", "Path to cortex.config.ts").default(
19
+ DEFAULT_CONFIG,
20
+ DEFAULT_CONFIG,
21
+ ),
22
+ )
23
+ .option("-v, --verbose", "Print full errors and the commands being run")
24
+ .version(pkg.version, "-V, --version", "Output the version number")
25
+ .helpOption("-h, --help", "Display help for command")
26
+ // The implicit `help [command]` would print as a third, ungrouped section and
27
+ // break the two-group screen the prototype fixes. `cortex <cmd> --help` covers it.
28
+ .helpCommand(false)
29
+ // Commander appends ` [options]` to any command that has some. Left in, the
30
+ // first command to grow a flag widens the whole list and the two-group screen
31
+ // in cli-output.md §1 stops matching. `cortex <command> --help` lists them.
32
+ .configureHelp({
33
+ subcommandTerm: function (cmd) {
34
+ return new Help().subcommandTerm(cmd).replace(" [options]", "");
35
+ },
36
+ })
37
+ .addHelpText("after", "\nRun `cortex <resource> --help` to see what you can do with one.");
38
+
39
+ // Each command owns its help group, so the two headings survive these files being
40
+ // rewritten. Registration order fixes the order the groups print in.
41
+ registerNew(program);
42
+ registerSwagger(program);
43
+ registerGraph(program);
44
+
45
+ // Commander's own errors print and exit on their own, and every failure the
46
+ // commands predict goes through `report.fail`. Anything still reaching here is a
47
+ // bug — in the CLI, or in the project's server — and it gets the same grammar
48
+ // rather than a raw Bun stack dump.
49
+ await program.parseAsync().catch(function (error: unknown) {
50
+ const verbose = program.opts().verbose === true;
51
+ const rerun = process.argv.slice(2);
52
+ if (!verbose) rerun.push("--verbose");
53
+
54
+ report.fail({
55
+ what: "cortex hit an unexpected error",
56
+ error,
57
+ blocks: [
58
+ "That is a bug — the CLI should have explained this. Report it at\nhttps://github.com/boring91/cortex/issues with the output of:",
59
+ ` cortex ${rerun.join(" ")}`,
60
+ ],
61
+ verbose,
62
+ });
63
+ });
@@ -0,0 +1,253 @@
1
+ import type { Command } from "@commander-js/extra-typings";
2
+ import {
3
+ GRAPH_SCHEMA,
4
+ GRAPH_SCHEMA_VERSION,
5
+ createEmbedder,
6
+ createNeo4jClient,
7
+ type DomainDef,
8
+ type EmbeddingProviderConfig,
9
+ type Neo4jClient,
10
+ type Neo4jConfig,
11
+ } from "@cortex/contracts/graph";
12
+ import pc from "picocolors";
13
+
14
+ import { configLabel, findConfig, loadConfig } from "@/config/load";
15
+ import { validateConfig } from "@/config/validate";
16
+ import { seedGraph } from "@/graph/seed";
17
+ import * as report from "@/ui/report";
18
+
19
+ const TITLE = "cortex graph seed";
20
+
21
+ /** Domain names are longer than swagger's finding labels, so the body is wider. */
22
+ const DOMAIN_COLUMN = 14;
23
+
24
+ /** A failing statement is worth one line: enough to identify, not enough to drown. */
25
+ const DETAIL_WIDTH = 60;
26
+
27
+ /** Only the parts of a loaded config `graph seed` reads. */
28
+ type ProjectConfig = {
29
+ neo4j?: Neo4jConfig;
30
+ embedding?: EmbeddingProviderConfig & { dimension: number };
31
+ knowledge?: { domains?: Record<string, DomainDef> };
32
+ agents?: Record<string, { knowledge?: { domains?: Record<string, DomainDef> } | null }>;
33
+ };
34
+
35
+ // Connection failures reach us as whatever `fetch` threw, so match on shape
36
+ // rather than on a driver's error class.
37
+ const UNREACHABLE =
38
+ /unable to connect|connection refused|econnrefused|failed to fetch|fetch failed|enotfound|getaddrinfo|timed out/i;
39
+
40
+ export function registerGraph(program: Command) {
41
+ const graph = program
42
+ .command("graph")
43
+ .helpGroup("Resources:")
44
+ .summary("The Neo4j knowledge graph")
45
+ .description(
46
+ "The Neo4j knowledge graph.\n\nReads `knowledge.domains` and `neo4j` from your cortex.config.ts.",
47
+ )
48
+ .helpCommand(false);
49
+
50
+ // No flags. `--domain <name>` is flag-shaped design work, not a flag:
51
+ // `expandDomains` resolves cross-domain references and phase 1 creates every
52
+ // node before any edge, so a subset seed risks dangling edges.
53
+ graph
54
+ .command("seed")
55
+ .description("Load concepts, endpoints, services, and rules into Neo4j")
56
+ .action(async function () {
57
+ // The globals live on the root command, which reaches this file
58
+ // untyped — `Command` here is the parameter type, not cli.ts's
59
+ // inferred one.
60
+ const { config: path, verbose = false } = program.opts() as {
61
+ config: string;
62
+ verbose?: boolean;
63
+ };
64
+
65
+ await seed({
66
+ path,
67
+ explicit: program.getOptionValueSource("config") !== "default",
68
+ verbose,
69
+ });
70
+ });
71
+ }
72
+
73
+ async function seed(options: { path: string; explicit: boolean; verbose: boolean }) {
74
+ const { verbose } = options;
75
+
76
+ // Discovery runs before the box opens: with no config there is no run to
77
+ // report on, only the reason there isn't one.
78
+ const configPath = findConfig({ ...options, command: TITLE });
79
+
80
+ report.header(TITLE);
81
+ const loaded = await loadConfig(configPath, verbose);
82
+ validateConfig(loaded, configPath);
83
+ report.input("Config", configLabel(configPath));
84
+
85
+ const config = loaded as ProjectConfig;
86
+
87
+ // Inputs are reported as they resolve, and only when the config has them:
88
+ // an absent section is `runSeed`'s error to throw, not ours to predict.
89
+ if (config.neo4j?.url) report.input("Neo4j", config.neo4j.url);
90
+ const domains = collectDomains(config);
91
+ const names = Object.keys(domains);
92
+ if (names.length) report.input("Domains", `${names.length} — ${names.join(", ")}`);
93
+
94
+ const startedAt = Date.now();
95
+ const result = await runSeed(config, domains).catch(function (error: unknown): never {
96
+ return failSeed({ error, config, configPath, verbose });
97
+ });
98
+
99
+ // A domain is the unit the user edits, so it is the unit the report counts
100
+ // against: a wrong number next to `attendance` points at src/domains/attendance/.
101
+ report.blank();
102
+ for (const [name, domain] of Object.entries(result.domains)) {
103
+ const counts = [report.plural(domain.nodes, "node"), report.plural(domain.edges, "edge")];
104
+ if (domain.failed) counts.push(`${domain.failed} failed`);
105
+ report.row(name, counts.join(" · "), DOMAIN_COLUMN);
106
+
107
+ // The first failure only: enough to identify what broke, not enough to
108
+ // drown the run — the count above says how many followed it.
109
+ const failure = domain.failures[0];
110
+ if (failure) {
111
+ report.row("", `${pc.red("✗")} ${truncate(failure.error)}`, DOMAIN_COLUMN);
112
+ report.row("", ` ${truncate(oneLine(failure.statement))}`, DOMAIN_COLUMN);
113
+ }
114
+ }
115
+
116
+ // Embeddings are a second pass over the whole graph, so they get their own
117
+ // step rather than a row among the counts.
118
+ report.blank();
119
+ report.input(
120
+ "Embeddings",
121
+ `${report.plural(result.embeddings.concepts, "concept")}, dimension ${result.embeddings.dimension}`,
122
+ );
123
+ report.blank();
124
+
125
+ const counts = [report.plural(result.nodes, "node"), report.plural(result.edges, "edge")];
126
+ if (result.failed) counts.push(`${result.failed} failed`);
127
+ report.footer(`${counts.join(" · ")} (${((Date.now() - startedAt) / 1000).toFixed(1)}s)`);
128
+
129
+ if (result.failed) {
130
+ report.note("The graph is partially seeded. Re-running is safe — seeding is MERGE-based.");
131
+ process.exit(1);
132
+ }
133
+ }
134
+
135
+ /** Cypher is generated multi-line and indented; a report row is neither. */
136
+ function oneLine(statement: string) {
137
+ return statement.replace(/\s+/g, " ").trim();
138
+ }
139
+
140
+ function truncate(text: string) {
141
+ return text.length > DETAIL_WIDTH ? `${text.slice(0, DETAIL_WIDTH - 2)} …` : text;
142
+ }
143
+
144
+ /** Server level first, then per agent — an agent's domain of the same name wins. */
145
+ function collectDomains(config: ProjectConfig) {
146
+ const all: Record<string, DomainDef> = { ...config.knowledge?.domains };
147
+
148
+ for (const agent of Object.values(config.agents ?? {})) {
149
+ Object.assign(all, agent.knowledge?.domains);
150
+ }
151
+
152
+ return all;
153
+ }
154
+
155
+ /**
156
+ * The whole pipeline is the CLI's, built on the graph contract it depends on:
157
+ * `@cortex/contracts/graph` is where the vocabulary, the Neo4j client and the embedder
158
+ * come from, and the version it seeds at is the version of that dependency.
159
+ */
160
+ async function runSeed(config: ProjectConfig, domains: Record<string, DomainDef>) {
161
+ if (!Object.keys(domains).length) throw new Error("No domains found in knowledge config");
162
+ if (!config.neo4j) throw new Error("neo4j config is required for graph seed");
163
+ if (!config.embedding) throw new Error("embedding config is required for graph seed");
164
+
165
+ const embed = createEmbedder(config.embedding);
166
+ const neo4j = createNeo4jClient(config.neo4j, embed);
167
+
168
+ await warnOnRestamp(neo4j);
169
+
170
+ return await seedGraph(
171
+ {
172
+ neo4j,
173
+ embedding: { embed, dimension: config.embedding.dimension },
174
+ schema: GRAPH_SCHEMA,
175
+ schemaVersion: GRAPH_SCHEMA_VERSION,
176
+ },
177
+ domains,
178
+ );
179
+ }
180
+
181
+ /**
182
+ * Seeding is MERGE-based, so a graph written under an older schema is not cleaned
183
+ * up — its nodes stay, under labels nothing reads. Worth saying out loud; not
184
+ * worth blocking on, since re-seeding is exactly how that graph gets fixed.
185
+ */
186
+ async function warnOnRestamp(neo4j: Neo4jClient) {
187
+ const rows: unknown = JSON.parse(
188
+ await neo4j.query(
189
+ `MATCH (g:${GRAPH_SCHEMA.label.marker}) RETURN g.${GRAPH_SCHEMA.prop.version} AS version`,
190
+ ),
191
+ );
192
+
193
+ // Anything but a row array means an unreachable or unreadable graph, which
194
+ // the seed itself is about to report far better than a warning could.
195
+ if (!Array.isArray(rows)) return;
196
+
197
+ const stamped = (rows as { version?: number }[])[0]?.version;
198
+ if (stamped === undefined || stamped === GRAPH_SCHEMA_VERSION) return;
199
+
200
+ report.blank();
201
+ report.row(
202
+ "",
203
+ `${pc.yellow("!")} Graph was written with schema v${stamped}; restamping as v${GRAPH_SCHEMA_VERSION}.`,
204
+ DOMAIN_COLUMN,
205
+ );
206
+ report.row("", " Nodes from the old schema are not removed.", DOMAIN_COLUMN);
207
+ }
208
+
209
+ /**
210
+ * Section requirements are `runSeed`'s to enforce — it already throws
211
+ * `"neo4j config is required for graph seed"`. This renders what it threw.
212
+ */
213
+ function failSeed(options: {
214
+ error: unknown;
215
+ config: ProjectConfig;
216
+ configPath: string;
217
+ verbose: boolean;
218
+ }): never {
219
+ const { error, config, configPath, verbose } = options;
220
+ const label = configLabel(configPath);
221
+ const message = error instanceof Error ? error.message : String(error);
222
+
223
+ if (message === "No domains found in knowledge config") {
224
+ report.fail({
225
+ what: `${label} has no knowledge.domains`,
226
+ blocks: [
227
+ "`graph seed` loads domain definitions into Neo4j. Add them:",
228
+ " knowledge: { domains: { leaves: leavesDomain } }",
229
+ "See https://github.com/boring91/cortex#file-structure-convention",
230
+ ],
231
+ });
232
+ }
233
+
234
+ const url = config.neo4j?.url;
235
+ if (url !== undefined && UNREACHABLE.test(message)) {
236
+ report.fail({
237
+ what: `Cannot reach Neo4j at ${url}`,
238
+ error,
239
+ blocks: [
240
+ "Is it running? docker compose up -d neo4j",
241
+ `Otherwise check neo4j.url / neo4j.user / neo4j.password in\n${label}.`,
242
+ ],
243
+ verbose,
244
+ });
245
+ }
246
+
247
+ report.fail({
248
+ what: "Could not seed the graph",
249
+ error,
250
+ blocks: [`Check neo4j, embedding, and knowledge.domains in ${label}.`],
251
+ verbose,
252
+ });
253
+ }