@m6d/cortex-cli 0.12.1 → 0.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 (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /package/src/{cc → management}/output.ts +0 -0
@@ -1,61 +1,128 @@
1
1
  /**
2
2
  * Every file `cortex new` writes, as one pure function. Files with nothing to
3
3
  * fill in are verbatim text under templates/; the ones that vary with the
4
- * selection are assembled here. The tooling configs are inlined rather than
4
+ * project are assembled here. The tooling configs are inlined rather than
5
5
  * pulled from `@m6d/eslint-config`, `@m6d/prettier-config` and `@m6d/tsconfig`:
6
6
  * those are `private: true` at `0.0.0` and resolvable only through this
7
7
  * workspace, and a standalone scaffold has no workspace.
8
8
  */
9
9
 
10
- import { FEATURE_NAMES, FEATURES, type Feature } from "@/scaffold/features";
11
- import indexTs from "@/scaffold/templates/index.ts.txt";
12
- import domainsReadme from "@/scaffold/templates/domains-README.md.txt";
10
+ import pkg from "../../package.json";
11
+ import serverTs from "@/scaffold/templates/server.ts.txt";
12
+ import setupTs from "@/scaffold/templates/setup.ts.txt";
13
+ import datasetsTs from "@/scaffold/templates/datasets.ts.txt";
13
14
  import tsconfigJson from "@/scaffold/templates/tsconfig.json.txt";
14
15
  import gitignoreText from "@/scaffold/templates/gitignore.txt";
16
+ import dockerignoreText from "@/scaffold/templates/dockerignore.txt";
15
17
  import eslintConfig from "@/scaffold/templates/eslint.config.mjs.txt";
16
18
  import prettierConfig from "@/scaffold/templates/prettier.config.mjs.txt";
17
19
  import fallowrc from "@/scaffold/templates/fallowrc.jsonc.txt";
20
+ // Project jargon is the project's to add. Only `mprocs`, which its own
21
+ // package.json names, is seeded: a stranger's vocabulary is noise in a repo.
22
+ import cspellJson from "@/scaffold/templates/cspell.json.txt";
18
23
  import { applyPorts, type Ports } from "@/scaffold/ports";
19
24
 
20
- export type Database = "postgres" | "mssql";
21
-
22
- // The generated project depends on these directly, not transitively: its own code
23
- // imports `zod` for tool schemas and `hono` for extra routes on `server.app`.
24
- // The ranges are packages/cortex-server's own peers, which is the compatibility
25
- // statement the server itself publishes, and a server range pinned to its
26
- // current minor, since a minor bump is where the server breaks; the release
27
- // sync (scripts/sync-package-versions.ts) restamps it and files.test.ts holds
28
- // them to that.
29
- export const DEPENDENCIES = {
30
- "@m6d/cortex-server": "~0.12.1",
31
- "@tanstack/ai": "^0.55.0",
32
- "@tanstack/ai-openai": "^0.22.8",
33
- hono: "^4.12.3",
34
- jose: "^6.1.3",
35
- openai: "^6",
36
- zod: "^4.1.8",
37
- };
25
+ /**
26
+ * The release of the Cortex image the generated stack runs. It and the
27
+ * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
28
+ * before the CLI publishes, so a scaffold pins what was current then.
29
+ */
30
+ const CORTEX_VERSION = "0.14.1";
31
+
32
+ /** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
33
+ export const DOCKER_HOST = "host.docker.internal";
34
+
35
+ /** How Cortex's containers reach the backend on this machine, at the port drawn for it. */
36
+ const SERVER_URL = `http://${DOCKER_HOST}:{{port.server}}`;
37
+
38
+ /** The longest server name Cortex accepts. */
39
+ const SERVER_NAME_MAX = 100;
40
+
41
+ /**
42
+ * The agent the manifest declares and the README sends you to chat with.
43
+ * Not `assistant`: Cortex holds that slug for its console's own assistant.
44
+ */
45
+ export const AGENT_SLUG = "helper";
46
+
47
+ /**
48
+ * The embedding model the stack asks for, and the dimension the image's
49
+ * migrations give the vector columns. `cortex setup` probes a provider with
50
+ * the same pair before reusing it for embeddings.
51
+ */
52
+ export const EMBEDDING = { model: "text-embedding-3-small", dimension: 768 } as const;
38
53
 
39
54
  const DEV_DEPENDENCIES = {
55
+ // `cortex apply`, `cortex setup` and the management commands, at the release that wrote the project.
56
+ "@m6d/cortex-cli": `~${pkg.version}`,
40
57
  "@types/bun": "latest",
58
+ // The two runners of `bun run dev` and `bun run dev:agent`.
59
+ concurrently: "^10.0.6",
41
60
  cspell: "^9.6.4",
42
61
  eslint: "^9",
43
62
  "eslint-config-prettier": "^10.1.8",
44
63
  "eslint-plugin-prettier": "^5.5.5",
45
64
  fallow: "^3.9.1",
65
+ mprocs: "^0.9.6",
46
66
  prettier: "^3.6.2",
47
67
  typescript: "^5",
48
68
  "typescript-eslint": "^8.65.0",
49
69
  };
50
70
 
51
- // Postgres is the one we run, so it gets a scaffolded port. MSSQL is normally an
52
- // existing instance nobody moved, which is why it keeps 1433 and has no service.
53
- const CONNECTION_STRING = {
54
- postgres: "postgres://postgres:postgres@localhost:{{port.postgres}}/cortex",
55
- mssql: "Server=localhost,1433;Database=cortex;User Id=sa;Password=your-password;Encrypt=false",
56
- } satisfies Record<Database, string>;
71
+ /**
72
+ * `setup` puts `cortex setup` among the processes `bun run dev` starts; `env`
73
+ * holds values `cortex new` was given for `.env`, such as the model provider,
74
+ * which is then written beside `.env.example` with them filled in.
75
+ */
76
+ type Input = {
77
+ name: string;
78
+ datasets: boolean;
79
+ setup: boolean;
80
+ ports: Ports;
81
+ env?: Record<string, string>;
82
+ };
83
+
84
+ /** The input, and the name the backend is registered under in Cortex. */
85
+ type Project = Input & { server: string };
86
+
87
+ /**
88
+ * `text` with each `KEY=` line named in `values` set to its value, and every
89
+ * other line left as written. `cortex new` fills `.env` with it, and
90
+ * `cortex setup` the `.env` it asks the model provider for.
91
+ */
92
+ export function withEnvValues(text: string, values: Record<string, string>) {
93
+ return Object.entries(values).reduce(
94
+ (filled, [key, value]) =>
95
+ filled.replace(new RegExp(`^${key}=.*$`, "m"), () => `${key}=${value}`),
96
+ text,
97
+ );
98
+ }
99
+
100
+ /** What `bun run dev` starts side by side, named as the runners label them. */
101
+ function devProcesses(setup: boolean) {
102
+ return {
103
+ cortex: "docker compose up",
104
+ server: "bun run dev:server",
105
+ ...(setup ? { setup: "bun cortex setup --wait --yes" } : {}),
106
+ };
107
+ }
108
+
109
+ /** One runner's command line: the process names, then each command quoted. */
110
+ function runAll(runner: "mprocs" | "concurrently", processes: Record<string, string>) {
111
+ const commands = Object.values(processes).map((command) => `"${command}"`);
112
+ return `${runner} --names ${Object.keys(processes).join(",")} ${commands.join(" ")}`;
113
+ }
114
+
115
+ /** The name as the manifest `id` takes it: lowercase words and hyphens. */
116
+ export function manifestIdOf(name: string) {
117
+ return name
118
+ .replace(/[^a-z0-9]+/g, "-")
119
+ .slice(0, 64)
120
+ .replace(/^-+|-+$/g, "");
121
+ }
122
+
123
+ function packageJson({ name, datasets, setup }: Project) {
124
+ const processes = devProcesses(setup);
57
125
 
58
- function packageJson(name: string) {
59
126
  return `${JSON.stringify(
60
127
  {
61
128
  name,
@@ -69,14 +136,26 @@ function packageJson(name: string) {
69
136
  // analyses — dead code, duplication, complexity — over everything.
70
137
  audit: "fallow",
71
138
  check: "tsc --noEmit",
72
- dev: "bun run --hot index.ts",
139
+ // The same processes twice: a dashboard for people, and one
140
+ // labelled log for coding agents and CI.
141
+ dev: runAll("mprocs", processes),
142
+ "dev:agent": runAll("concurrently", processes),
143
+ "dev:server": "bun run --hot index.ts",
144
+ // A no-op where setup.ts is not copied in, as in an image build.
145
+ postinstall: "bun run --if-present setup.ts",
73
146
  format: "prettier --write .",
74
147
  "format:check": "prettier --check .",
75
148
  lint: "eslint",
76
149
  spellcheck: 'cspell "**/*.{ts,mjs,json,md}"',
77
150
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
78
151
  },
79
- dependencies: DEPENDENCIES,
152
+ dependencies: {
153
+ "@m6d/cortex": "~0.2.0",
154
+ // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
155
+ ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
156
+ hono: "^4.12.3",
157
+ zod: "^4.6.5",
158
+ },
80
159
  devDependencies: DEV_DEPENDENCIES,
81
160
  },
82
161
  null,
@@ -84,275 +163,359 @@ function packageJson(name: string) {
84
163
  )}\n`;
85
164
  }
86
165
 
87
- /**
88
- * Never toggled, so they only ever appear commented out: `context` is fully
89
- * defaulted, and `reranker` is what you reach for once graph retrieval comes
90
- * back noisy, not on day one.
91
- */
92
- const NEVER_SCAFFOLDED = [
93
- [
94
- "Re-ranks knowledge-graph retrieval when it comes back noisy.",
95
- `reranker: {
96
- url: process.env["CORTEX_RERANKER_URL"]!,
97
- apiKey: process.env["CORTEX_RERANKER_KEY"]!,
98
- },`,
99
- ],
100
- [
101
- "Context-window tuning. Every field is defaulted, so override only what you\nhave measured a reason to change.",
102
- `context: {
103
- maxContextTokens: 120_000,
104
- recentMessagesToKeep: 6,
105
- },`,
106
- ],
107
- ] satisfies [note: string, config: string][];
108
-
109
- /** Fragments are authored at zero indentation and placed by these two. */
110
- function indented(block: string) {
111
- return block.replace(/^(?!$)/gm, " ");
112
- }
166
+ function indexTs({ datasets }: Project) {
167
+ const routes = datasets
168
+ ? "its server protocol under /cortex and its\n * dataset host under /datasets; your own routes go beside them"
169
+ : "its server protocol under /cortex; your own routes go beside it";
113
170
 
114
- function commented(block: string) {
115
- return block.replace(/^(?!$)/gm, " // ");
116
- }
117
-
118
- /** A note, then its fragment — written live, or written disabled. */
119
- function block(note: string, config: string, enabled: boolean) {
120
- return `\n${commented(note)}\n${enabled ? indented(config) : commented(config)}\n`;
121
- }
171
+ return `import { Hono } from "hono";
122
172
 
123
- function cortexConfig(database: Database, features: Feature[]) {
124
- const fragment = (feature: Feature) =>
125
- block(FEATURES[feature].note, FEATURES[feature].config, features.includes(feature));
126
-
127
- const on = features.map(fragment).join("");
128
- const off = FEATURE_NAMES.filter((feature) => !features.includes(feature))
129
- .map(fragment)
130
- .join("");
131
- const never = NEVER_SCAFFOLDED.map(([note, config]) => block(note, config, false)).join("");
132
-
133
- // cspell.json ships an empty word list on purpose, so the Cortex jargon the
134
- // config names teaches the checker from the file that uses it.
135
- return `import { defineAgent } from "@m6d/cortex-server";
136
- import type { CortexConfig } from "@m6d/cortex-server";
137
-
138
- // cspell:words reranker
173
+ ${datasets ? `import { datasets } from "./datasets";\n` : ""}import { server } from "./server";
139
174
 
140
175
  /**
141
- * Inert on purpose: this file default-exports data and nothing else, so importing
142
- * it never opens a connection or binds a port. Construction lives in index.ts,
143
- * which is what lets tooling read this config without booting the server.
176
+ * This backend. Cortex reaches ${routes}.
144
177
  */
145
- export default {
146
- port: 3331,
147
- database: {
148
- type: "${database}",
149
- connectionString: process.env["CORTEX_DATABASE_URL"]!,
150
- },
151
- model: {
152
- baseURL: process.env["CORTEX_MODEL_URL"]!,
153
- apiKey: process.env["CORTEX_MODEL_KEY"]!,
154
- modelName: process.env["CORTEX_MODEL_NAME"]!,
155
- },
156
- ${on === "" ? "" : `${on}\n`} agents: {
157
- assistant: defineAgent({
158
- systemPrompt: "You are a helpful assistant.",
159
- // Applications that serve datasets for grouped questions (see the
160
- // README's "Dataset hosts"): the host applies its own access rules
161
- // under the run's token the list below forwards, the agent only
162
- // picks which datasets to expose and pin.
163
- // datasetHosts: [{ name: "store", url: "https://store.example/api/datasets", credentials: [{ header: "X-End-User-Token", credential: { kind: "run-token" } }], pinned: ["orders"] }],
164
- }),
165
- },
166
-
167
- // Everything below is off, written out so the file documents what else the
168
- // server does. Uncomment a block and fill in its keys in .env to enable it.
169
- ${off}${never}} satisfies CortexConfig;
178
+ const app = new Hono();
179
+ app.route("/cortex", server);
180
+ ${datasets ? `app.route("/datasets", datasets);\n` : ""}
181
+ export default { port: {{port.server}}, fetch: app.fetch };
170
182
  `;
171
183
  }
172
184
 
173
- /**
174
- * Whether this selection runs anything of its own — which is the same question as
175
- * "did any port get chosen", since a chosen port is always some service's.
185
+ function manifestTs({ name, datasets }: Project) {
186
+ const fromEnv = datasets
187
+ ? `/** The server this backend is registered as, and where Cortex reaches it, from .env. */
188
+ const { CORTEX_SERVER_NAME: server, CORTEX_SERVER_URL: serverUrl } = process.env;
189
+ if (!server || !serverUrl) {
190
+ throw new Error("Apply from this project: its .env names the server.");
191
+ }`
192
+ : `/** The server this backend is registered as, from .env. */
193
+ const server = process.env["CORTEX_SERVER_NAME"];
194
+ if (!server) throw new Error("Apply from this project: its .env names the server.");`;
195
+ const datasetHosts = `
196
+ // The orders this backend serves (datasets.ts), for every agent on its server.
197
+ datasetHosts: [
198
+ {
199
+ name: "store",
200
+ url: \`\${serverUrl}/datasets\`,
201
+ credentials: [],
202
+ expose: ["orders"],
203
+ pinned: ["orders"],
204
+ audience: { server },
205
+ },
206
+ ],`;
207
+
208
+ return `import { defineManifest } from "@m6d/cortex";
209
+
210
+ /*
211
+ * What this project declares in Cortex, which \`cortex apply\` makes Cortex
212
+ * match. The agent runs on this backend's server, whose variable its prompt
213
+ * reads and whose native tool it calls.
176
214
  */
177
- function hasServices(database: Database, features: Feature[]) {
178
- return (
179
- database === "postgres" ||
180
- features.some((feature) => FEATURES[feature].compose !== undefined)
181
- );
215
+
216
+ ${fromEnv}
217
+
218
+ export default defineManifest({
219
+ id: ${JSON.stringify(manifestIdOf(name))},
220
+ agents: [
221
+ {
222
+ slug: "${AGENT_SLUG}",
223
+ name: "Helper",
224
+ server,
225
+ prompt: "You are a helpful assistant. Address the user as {{userName}}.",
226
+ // No identity provider yet: every caller is one shared identity.
227
+ auth: { kind: "anonymous" },
228
+ },
229
+ ],${datasets ? datasetHosts : ""}
230
+ });
231
+ `;
182
232
  }
183
233
 
184
- function env(database: Database, features: Feature[]) {
185
- const blocks = features.map((feature) => `\n${FEATURES[feature].env}\n`);
186
-
187
- // Only the URLs pointing at scaffolded services carry a chosen port, and those
188
- // are exactly the ones docker-compose.yml publishes.
189
- const note = hasServices(database, features)
190
- ? `#
191
- # The service ports below are not the canonical ones. They were picked when this
192
- # project was scaffolded, from what was free on the machine, so it does not
193
- # collide with anything else you run. Change them freely — in step with
194
- # docker-compose.yml.
195
- `
196
- : "";
197
-
198
- return `# Environment for this Cortex server. Bun loads this file from the directory you
199
- # run in, so it has to sit next to cortex.config.ts.
234
+ /** Every setting the project has, which setup.ts copies into .env, generating the secrets. */
235
+ function envExample({ server }: Project) {
236
+ return `# Every setting this project has. Bun loads .env from the directory you run
237
+ # in, into the backend and the \`cortex\` CLI; docker compose hands it to the
238
+ # Cortex containers and reads COMPOSE_PROJECT_NAME from it.
200
239
  #
201
- # .env.example is the committed copy. Keep the two in step; keep secrets in .env.
202
- ${note}
203
- # --- Model -------------------------------------------------------------------
204
- # Any OpenAI-compatible endpoint: base URL, key, and model name.
205
- CORTEX_MODEL_URL=https://api.openai.com/v1
206
- CORTEX_MODEL_KEY=your-model-api-key
207
- CORTEX_MODEL_NAME=your-model-name
208
-
209
- # --- Database ----------------------------------------------------------------
210
- # The dialect is ${database}, set in cortex.config.ts. Change it there, not here.
211
- CORTEX_DATABASE_URL=${CONNECTION_STRING[database]}
212
- ${blocks.join("")}`;
240
+ # This is the committed copy. \`bun install\` writes .env from it, generating
241
+ # the secrets it leaves blank, and never changes a value .env already holds.
242
+
243
+ # The Compose project this checkout's Cortex stack runs as, drawn for each
244
+ # checkout, so a clone never shares another checkout's containers or volumes.
245
+ COMPOSE_PROJECT_NAME=
246
+
247
+ # Cortex's first admin, created when the stack first starts, and its
248
+ # management key.
249
+ DEFAULT_ADMIN_EMAIL=admin@example.com
250
+ DEFAULT_ADMIN_PASSWORD=
251
+ BOOTSTRAP_MANAGEMENT_KEY=
252
+ # Session signing, and the key the secrets store is encrypted with.
253
+ BETTER_AUTH_SECRET=
254
+ SECRETS_MASTER_KEY=
255
+ # Any OpenAI-compatible embeddings endpoint and model. Tool search and
256
+ # knowledge collections embed with it, and stay off while the key is blank.
257
+ EMBEDDING_BASE_URL=https://api.openai.com/v1
258
+ EMBEDDING_API_KEY=
259
+ EMBEDDING_MODEL=${EMBEDDING.model}
260
+ # The private hosts Cortex's containers may call, and send a secret to over
261
+ # plain http, comma-separated: this machine, for the backend. A model provider
262
+ # on a private network goes here too. The policy admits a host by name,
263
+ # whatever the port: see README.md.
264
+ EGRESS_ALLOWED_HOSTS=${DOCKER_HOST}
265
+
266
+ # The Cortex \`cortex apply\` and \`cortex setup\` talk to: this checkout's,
267
+ # under its management key, so neither needs \`cortex login\`. \`-p <profile>\`
268
+ # still picks a saved one.
269
+ CORTEX_URL=http://localhost:{{port.cortex}}
270
+ CORTEX_KEY=\${BOOTSTRAP_MANAGEMENT_KEY}
271
+
272
+ # This backend as Cortex registers it: its name, where Cortex's containers
273
+ # reach it, and the key Cortex presents on every call to it.
274
+ CORTEX_SERVER_NAME=${server}
275
+ CORTEX_SERVER_URL=${SERVER_URL}
276
+ CORTEX_SERVER_KEY=
277
+
278
+ # The model provider \`cortex setup\` registers and the server's agents run on.
279
+ # While these are blank, setup registers the server and applies nothing.
280
+ MODEL_PROVIDER_NAME=
281
+ MODEL_PROVIDER_URL=
282
+ MODEL_PROVIDER_API_KEY=
283
+ MODEL_PROVIDER_MODEL=
284
+ `;
213
285
  }
214
286
 
215
- /**
216
- * Services the selections actually need, and nothing else. `auth` and the
217
- * Control Center are external, the embedding endpoint is a URL like the main
218
- * model, and MSSQL is normally an existing instance — so with `--yes --database
219
- * mssql` there is no service at all, and no file is written.
220
- */
221
- function dockerCompose(name: string, database: Database, features: Feature[]) {
222
- const services: string[] = [];
223
- const volumes: string[] = [];
287
+ /** Cortex itself: its app, worker and engine, all on the one Cortex image, and their stores. */
288
+ function dockerCompose() {
289
+ return `# Cortex, for this project's agents to run on: the console at
290
+ # http://localhost:{{port.cortex}}, the engine widgets and trigger callers reach at
291
+ # http://localhost:{{port.engine}}, and the stores they keep state in. \`bun run dev\`
292
+ # starts it; .env holds its secrets.
293
+ #
294
+ # The host ports were picked at scaffold time from what was free on this
295
+ # machine. Move one and the URLs that name it here, in .env and in README.md
296
+ # follow.
297
+ # CORTEX_VERSION, set in the shell, runs another release of the image.
298
+ #
299
+ # The console's browser uploads (knowledge documents) go straight to
300
+ # S3_ENDPOINT, which only this stack resolves. Point it at storage the
301
+ # browser reaches too before relying on them.
302
+
303
+ # The stack runs as .env's COMPOSE_PROJECT_NAME, drawn for each checkout by
304
+ # setup.ts, so no two checkouts share containers or volumes, whatever their
305
+ # directories are called.
306
+
307
+ x-cortex: &cortex
308
+ image: syscodeia/cortex:\${CORTEX_VERSION:-${CORTEX_VERSION}}
309
+ working_dir: /repo/apps/cortex
310
+ # The whole .env, the backend's settings too. What \`environment\` sets
311
+ # wins over it, so nothing there can move these containers' ports or mode.
312
+ env_file: .env
313
+ environment: &cortex-environment
314
+ NODE_ENV: production
315
+ PORT: "3000"
316
+ DATABASE_URL: postgres://postgres:postgres@postgres:5432/cortex
317
+ REDIS_URL: redis://redis:6379
318
+ S3_ENDPOINT: http://minio:9000
319
+ S3_REGION: us-east-1
320
+ S3_ACCESS_KEY_ID: minio
321
+ S3_SECRET_ACCESS_KEY: minio-secret-key
322
+ S3_BUCKET: cortex
323
+ BETTER_AUTH_ALLOWED_HOSTS: localhost:{{port.cortex}},127.0.0.1:{{port.cortex}}
324
+ # The dimension the image's migrations give the vector columns.
325
+ EMBEDDING_DIMENSION: "${EMBEDDING.dimension}"
326
+ # The engine's public origin (browsers, trigger callers) and the
327
+ # address the app reaches it at inside the stack.
328
+ ENGINE_PUBLIC_URL: http://localhost:{{port.engine}}
329
+ ENGINE_INTERNAL_URL: http://engine:3001
330
+ # Docker Desktop resolves host.docker.internal on its own; Linux needs this.
331
+ extra_hosts:
332
+ - host.docker.internal:host-gateway
224
333
 
225
- if (database === "postgres") {
226
- services.push(` postgres:
227
- image: postgres:17-alpine
334
+ services:
335
+ postgres:
336
+ image: pgvector/pgvector:pg17
337
+ restart: unless-stopped
228
338
  environment:
229
339
  POSTGRES_USER: postgres
230
340
  POSTGRES_PASSWORD: postgres
231
341
  POSTGRES_DB: cortex
232
- ports:
233
- - "{{port.postgres}}:5432"
234
342
  volumes:
235
- - postgres-data:/var/lib/postgresql/data`);
236
- volumes.push("postgres-data");
237
- }
238
-
239
- for (const feature of features) {
240
- const { compose, volume } = FEATURES[feature];
241
- if (compose === undefined) continue;
242
- services.push(compose);
243
- if (volume !== undefined) volumes.push(volume);
244
- }
245
-
246
- if (services.length === 0) return undefined;
247
-
248
- const volumeBlock =
249
- volumes.length === 0
250
- ? ""
251
- : `\nvolumes:\n${volumes.map((name) => ` ${name}:\n`).join("")}`;
252
-
253
- return `# Local infrastructure for this project. The .env this was scaffolded with
254
- # already points at these, so \`docker compose up -d\` is enough.
255
- #
256
- # The host ports were picked at scaffold time from what was free on the machine —
257
- # the container ports next to them are the canonical ones and should stay put.
258
- # Move a host port and .env has to follow.
259
-
260
- # Explicit so the project never collides with another checkout whose directory
261
- # happens to share this one's name (Compose defaults to the directory name, and
262
- # a name clash silently replaces the other project's containers and volumes).
263
- name: ${name}-cortex-server
264
-
265
- services:
266
- ${services.join("\n\n")}
267
- ${volumeBlock}`;
343
+ - postgres-data:/var/lib/postgresql/data
344
+ healthcheck:
345
+ test: ["CMD-SHELL", "pg_isready -U postgres -d cortex"]
346
+ interval: 5s
347
+ timeout: 5s
348
+ retries: 10
349
+
350
+ redis:
351
+ image: redis:7-alpine
352
+ restart: unless-stopped
353
+ healthcheck:
354
+ test: ["CMD", "redis-cli", "ping"]
355
+ interval: 5s
356
+ timeout: 5s
357
+ retries: 10
358
+
359
+ minio:
360
+ # A community rebuild of MinIO's last free release, pinned by digest:
361
+ # MinIO's own images left Docker Hub. It runs as nobody, so it takes
362
+ # root to reuse a volume another image wrote.
363
+ image: sourcemation/minio@sha256:538065d2dd0ef360fe3f22f25d2384e6bae9075b6d2603cb5587193bd73e6276
364
+ user: "0:0"
365
+ restart: unless-stopped
366
+ command: server /data
367
+ environment:
368
+ MINIO_ROOT_USER: minio
369
+ MINIO_ROOT_PASSWORD: minio-secret-key
370
+ volumes:
371
+ - minio-data:/data
372
+ healthcheck:
373
+ test: ["CMD", "bash", "-c", "</dev/tcp/127.0.0.1/9000"]
374
+ interval: 5s
375
+ timeout: 5s
376
+ retries: 10
377
+
378
+ minio-init:
379
+ image: chainguard/minio-client@sha256:b2bd7824d23d3e3b15bedd7e87fbc3be29d2e213307b4f901e4a1d92356dc20f
380
+ depends_on:
381
+ minio:
382
+ condition: service_healthy
383
+ environment:
384
+ MC_HOST_local: http://minio:minio-secret-key@minio:9000
385
+ command: ["mb", "--ignore-existing", "local/cortex"]
386
+
387
+ # One-shot: migrate the database, then create the first admin and its key.
388
+ migrator:
389
+ <<: *cortex
390
+ command: ["bun", "run", "db:migrate"]
391
+ depends_on:
392
+ postgres:
393
+ condition: service_healthy
394
+
395
+ seeder:
396
+ <<: *cortex
397
+ command: ["bun", "run", "db:seed:admin"]
398
+ depends_on:
399
+ migrator:
400
+ condition: service_completed_successfully
401
+
402
+ # The console and the management API.
403
+ app:
404
+ <<: *cortex
405
+ restart: unless-stopped
406
+ ports:
407
+ - "127.0.0.1:{{port.cortex}}:3000"
408
+ depends_on:
409
+ seeder:
410
+ condition: service_completed_successfully
411
+ minio-init:
412
+ condition: service_completed_successfully
413
+
414
+ worker:
415
+ <<: *cortex
416
+ restart: unless-stopped
417
+ command: ["bun", "src/jobs/worker.ts"]
418
+ depends_on:
419
+ seeder:
420
+ condition: service_completed_successfully
421
+ minio-init:
422
+ condition: service_completed_successfully
423
+ redis:
424
+ condition: service_healthy
425
+
426
+ # Runs the agents: widgets, trigger callers and the console's playground
427
+ # and assistant talk to it.
428
+ engine:
429
+ <<: *cortex
430
+ restart: unless-stopped
431
+ command: ["bun", "src/engine/server.ts"]
432
+ environment:
433
+ <<: *cortex-environment
434
+ PORT: "3001"
435
+ # Browser origins allowed to call the engine: the console's, for its playground and assistant.
436
+ ENGINE_CORS_ORIGINS: http://localhost:{{port.cortex}},http://127.0.0.1:{{port.cortex}}
437
+ ports:
438
+ - "127.0.0.1:{{port.engine}}:3001"
439
+ depends_on:
440
+ seeder:
441
+ condition: service_completed_successfully
442
+ redis:
443
+ condition: service_healthy
444
+
445
+ volumes:
446
+ postgres-data:
447
+ minio-data:
448
+ `;
268
449
  }
269
450
 
270
- function cspellConfig() {
271
- return `${JSON.stringify(
272
- {
273
- version: "0.2",
274
- language: "en",
275
- // Yours to fill in. Project jargon belongs here; nothing is seeded,
276
- // because a stranger's vocabulary is noise in your repo.
277
- words: [],
278
- ignorePaths: [
279
- "node_modules/**",
280
- "out/**",
281
- "build/**",
282
- "**/dist/**",
283
- "coverage/**",
284
- "*.tsbuildinfo",
285
- "bun.lock",
286
- "package-lock.json",
287
- "pnpm-lock.yaml",
288
- "yarn.lock",
289
- "*.db",
290
- "*.sqlite",
291
- "scratch.*",
292
- ".env*",
293
- "**/*.min.js",
294
- "**/*.min.css",
295
- "**/db/migrations/**",
296
- "**/i18n/**",
297
- "docs",
298
- ".claude",
299
- ".angular",
300
- ".vercel/**",
301
- "vendor/**",
302
- ".gitignore",
303
- "**/domains/**/*.concept.ts",
304
- ],
305
- dictionaries: [
306
- "typescript",
307
- "node",
308
- "npm",
309
- "html",
310
- "css",
311
- "bash",
312
- "companies",
313
- "softwareTerms",
314
- ],
315
- },
316
- null,
317
- 4,
318
- )}\n`;
319
- }
451
+ function readme({ name, server, datasets, setup }: Project) {
452
+ const gettingStarted = setup
453
+ ? `That starts Cortex (\`docker compose up\`), this backend, and \`cortex setup\` in one dashboard.
454
+ Setup waits for Cortex, registers this backend as the server \`${server}\`, adds the model provider
455
+ in \`.env\`, and applies the manifest. Then it prints where to chat with \`${AGENT_SLUG}\`, and the first
456
+ time, how to sign in. It adds only what is missing, so every \`bun run dev\` runs it again.
457
+
458
+ While \`MODEL_PROVIDER_*\` in \`.env\` is blank, setup registers the server and applies nothing:
459
+ Cortex publishes no agent without a model. Run \`bun cortex setup\` in a terminal to be asked for
460
+ one.`
461
+ : `That starts Cortex (\`docker compose up\`) and this backend in one dashboard. Then register the
462
+ backend and apply the manifest, either way:
463
+
464
+ - \`bun cortex setup\` in a terminal asks for a model provider, registers the server
465
+ \`${server}\`, and applies the manifest.
466
+ - By hand: open the console at http://localhost:{{port.cortex}} and sign in with
467
+ \`DEFAULT_ADMIN_EMAIL\` and \`DEFAULT_ADMIN_PASSWORD\` from \`.env\`. Add a model provider on the
468
+ **Model providers** page, and store \`CORTEX_SERVER_KEY\` from \`.env\` on the **Secrets** page.
469
+ On the **Servers** page, add a server named \`${server}\` at \`${SERVER_URL}/cortex\` with that
470
+ secret as its key, and pick the model its agents run on. Then run \`bun cortex apply\`.`;
320
471
 
321
- function readme(name: string, database: Database, features: Feature[], compose: boolean) {
322
472
  return `# ${name}
323
473
 
324
- A Cortex server: one agent, a ${database} database, and an OpenAI-compatible model.${
325
- features.length === 0 ? "" : ` Also on: ${features.join(", ")}.`
326
- }
474
+ A backend for Cortex agents, and Cortex itself to run them on.
475
+
476
+ - \`server.ts\` is this backend's Cortex server: the \`userName\` variable the agents' prompts read,
477
+ and the \`rollDice\` tool it runs for them.${
478
+ datasets
479
+ ? "\n- `datasets.ts` serves an `orders` table as a dataset host, for agents to ask grouped\n questions of."
480
+ : ""
481
+ }
482
+ - \`cortex.manifest.ts\` declares the \`${AGENT_SLUG}\` agent on this server${
483
+ datasets ? " and the dataset host" : ""
484
+ }. \`cortex apply\` makes Cortex match it.
485
+ - \`docker-compose.yml\` runs Cortex: its app, worker and engine, and the stores they use.
486
+ - \`.env\` holds every setting: Cortex's secrets, this backend's key, and the model provider.
487
+ \`setup.ts\` runs on every \`bun install\`: it writes \`.env\` from the committed \`.env.example\` and
488
+ generates each secret left blank, so a fresh clone gets its own secrets and its own Compose stack.
489
+ It never changes a value already set. \`.dockerignore\` keeps both files out of any image built
490
+ here.
327
491
 
328
492
  ## Getting started
329
493
 
494
+ On a fresh clone, run \`bun install\` first (\`cortex new\` already did): it writes \`.env\`.
495
+
330
496
  \`\`\`sh
331
- bun install
332
- ${compose ? "docker compose up -d\n" : ""}# fill in CORTEX_MODEL_* in .env
333
497
  bun run dev
334
498
  \`\`\`
335
499
 
336
- The server listens on http://localhost:3331. Change \`port\` in \`cortex.config.ts\` to move it.${
337
- compose
338
- ? `
500
+ ${gettingStarted}
339
501
 
340
- The service ports in \`docker-compose.yml\` are not the canonical ones: they were picked at scaffold
341
- time from what was free on this machine, so this project does not fight whatever else you run.
342
- \`.env\` already points at them.`
343
- : ""
344
- }
502
+ Then chat with \`${AGENT_SLUG}\` on the console's **Playground** page. Edit \`cortex.manifest.ts\` and run
503
+ \`bun cortex apply\` to change it; an apply changes only what the edit changed. The CLI reaches this
504
+ checkout's Cortex through \`CORTEX_URL\` and \`CORTEX_KEY\` in \`.env\`, so it needs no login, and
505
+ \`-p <profile>\` picks a saved one instead.
345
506
 
346
- ## Layout
507
+ \`bun run dev:agent\` runs the same processes without the dashboard, each log line labelled with
508
+ its process, for coding agents and CI.
347
509
 
348
- - \`cortex.config.ts\` — inert data. It default-exports a \`CortexConfig\` and never constructs
349
- anything, so tooling can import it without booting a server.
350
- - \`index.ts\` — construction. It reads the config, calls \`createCortex\`, serves, and is where your
351
- own routes go.
352
- - \`.env\` — local secrets, loaded by Bun from the directory you run in. \`.env.example\` is the
353
- committed copy.
510
+ The backend listens on http://localhost:{{port.server}}. Move it in \`index.ts\`, and
511
+ \`CORTEX_SERVER_URL\` and the server's URL in Cortex with it.
354
512
 
355
- Commented-out sections in \`cortex.config.ts\` document what else the server can do.
513
+ ## Reaching this machine
514
+
515
+ Cortex's containers reach the backend at \`host.docker.internal\`, which \`EGRESS_ALLOWED_HOSTS\` in
516
+ \`.env\` admits. The policy admits a host by name, not by port, so every HTTP tool, MCP
517
+ server and credential you define in Cortex can reach any port this machine listens
518
+ on, over plain http. Keep the stack on your own machine, and define only tools you trust.
356
519
 
357
520
  ## Checks
358
521
 
@@ -363,36 +526,37 @@ pre-commit hook or CI job should run. Each is also available on its own: \`forma
363
526
  }
364
527
 
365
528
  /**
366
- * Relative path → contents. Nothing here touches the filesystem. Every
367
- * generator lists features in catalogue order, whatever order they came in.
529
+ * Relative path → contents. Nothing here touches the filesystem; the ports
530
+ * are drawn by the caller.
368
531
  */
369
- export function scaffoldFiles(name: string, database: Database, selected: Feature[], ports: Ports) {
370
- const features = FEATURE_NAMES.filter((feature) => selected.includes(feature));
371
- const envFile = env(database, features);
372
- const compose = dockerCompose(name, database, features);
373
-
532
+ export function scaffoldFiles(input: Input) {
533
+ const project = { ...input, server: input.name.slice(0, SERVER_NAME_MAX) };
374
534
  const files = {
375
- "cortex.config.ts": cortexConfig(database, features),
376
- "index.ts": indexTs,
377
- "package.json": packageJson(name),
535
+ "server.ts": serverTs,
536
+ "setup.ts": setupTs,
537
+ ...(project.datasets ? { "datasets.ts": datasetsTs } : {}),
538
+ "index.ts": indexTs(project),
539
+ "cortex.manifest.ts": manifestTs(project),
540
+ "docker-compose.yml": dockerCompose(),
541
+ "package.json": packageJson(project),
378
542
  "tsconfig.json": tsconfigJson,
379
- ".env": envFile,
380
- ".env.example": envFile,
543
+ ".env.example": envExample(project),
544
+ ...(project.env ? { ".env": withEnvValues(envExample(project), project.env) } : {}),
381
545
  ".gitignore": gitignoreText,
382
- "README.md": readme(name, database, features, compose !== undefined),
546
+ ".dockerignore": dockerignoreText,
547
+ "README.md": readme(project),
383
548
  "eslint.config.mjs": eslintConfig,
384
549
  "prettier.config.mjs": prettierConfig,
385
- "cspell.json": cspellConfig(),
550
+ "cspell.json": cspellJson,
386
551
  ".fallowrc.jsonc": fallowrc,
387
- ...(compose === undefined ? {} : { "docker-compose.yml": compose }),
388
- // The directory `domainsDir` defaults to, which makes the convention true
389
- // by construction: `cortex swagger sync` finds it with no config key.
390
- ...(features.includes("knowledge") ? { "src/domains/README.md": domainsReadme } : {}),
391
552
  } satisfies Record<string, string>;
392
553
 
393
554
  // One pass at the end, over everything: no generator holds a port number of
394
- // its own, so compose, .env and the README cannot disagree about one.
555
+ // its own, so compose and the README cannot disagree about one.
395
556
  return Object.fromEntries(
396
- Object.entries(files).map(([path, contents]) => [path, applyPorts(contents, ports)]),
557
+ Object.entries(files).map(([path, contents]) => [
558
+ path,
559
+ applyPorts(contents, project.ports),
560
+ ]),
397
561
  );
398
562
  }