@m6d/cortex-cli 0.12.1 → 0.13.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 (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -62
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
@@ -1,42 +1,47 @@
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";
18
20
  import { applyPorts, type Ports } from "@/scaffold/ports";
19
21
 
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
- };
22
+ /**
23
+ * The release of the Cortex image the generated stack runs. It and the
24
+ * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
25
+ * before the CLI publishes, so a scaffold pins what was current then.
26
+ */
27
+ const CORTEX_VERSION = "0.13.0";
28
+
29
+ /** Where the backend listens, and how Cortex's containers reach it on this machine. */
30
+ const SERVER_PORT = 3331;
31
+ const SERVER_URL = `http://host.docker.internal:${SERVER_PORT}`;
32
+
33
+ /** The longest server name Cortex accepts. */
34
+ const SERVER_NAME_MAX = 100;
35
+
36
+ /**
37
+ * The agent the manifest declares and the README sends you to chat with.
38
+ * Not `assistant`: Cortex holds that slug for its console's own assistant.
39
+ */
40
+ const AGENT_SLUG = "helper";
38
41
 
39
42
  const DEV_DEPENDENCIES = {
43
+ // `cortex apply` and the management commands, at the release that wrote the project.
44
+ "@m6d/cortex-cli": `~${pkg.version}`,
40
45
  "@types/bun": "latest",
41
46
  cspell: "^9.6.4",
42
47
  eslint: "^9",
@@ -48,14 +53,20 @@ const DEV_DEPENDENCIES = {
48
53
  "typescript-eslint": "^8.65.0",
49
54
  };
50
55
 
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>;
56
+ type Input = { name: string; datasets: boolean; ports: Ports };
57
+
58
+ /** The input, and the name the backend is registered under in Cortex. */
59
+ type Project = Input & { server: string };
60
+
61
+ /** The name as the manifest `id` takes it: lowercase words and hyphens. */
62
+ export function manifestIdOf(name: string) {
63
+ return name
64
+ .replace(/[^a-z0-9]+/g, "-")
65
+ .slice(0, 64)
66
+ .replace(/^-+|-+$/g, "");
67
+ }
57
68
 
58
- function packageJson(name: string) {
69
+ function packageJson({ name, datasets }: Project) {
59
70
  return `${JSON.stringify(
60
71
  {
61
72
  name,
@@ -70,13 +81,21 @@ function packageJson(name: string) {
70
81
  audit: "fallow",
71
82
  check: "tsc --noEmit",
72
83
  dev: "bun run --hot index.ts",
84
+ // A no-op where setup.ts is not copied in, as in an image build.
85
+ postinstall: "bun run --if-present setup.ts",
73
86
  format: "prettier --write .",
74
87
  "format:check": "prettier --check .",
75
88
  lint: "eslint",
76
89
  spellcheck: 'cspell "**/*.{ts,mjs,json,md}"',
77
90
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
78
91
  },
79
- dependencies: DEPENDENCIES,
92
+ dependencies: {
93
+ "@m6d/cortex": "~0.1.0",
94
+ // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
95
+ ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
96
+ hono: "^4.12.3",
97
+ zod: "^4.6.5",
98
+ },
80
99
  devDependencies: DEV_DEPENDENCIES,
81
100
  },
82
101
  null,
@@ -84,187 +103,270 @@ function packageJson(name: string) {
84
103
  )}\n`;
85
104
  }
86
105
 
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
- }
113
-
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
- }
122
-
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("");
106
+ function indexTs({ datasets }: Project) {
107
+ const routes = datasets
108
+ ? "its server protocol under /cortex and its\n * dataset host under /datasets; your own routes go beside them"
109
+ : "its server protocol under /cortex; your own routes go beside it";
132
110
 
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";
111
+ return `import { Hono } from "hono";
137
112
 
138
- // cspell:words reranker
113
+ ${datasets ? `import { datasets } from "./datasets";\n` : ""}import { server } from "./server";
139
114
 
140
115
  /**
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.
116
+ * This backend. Cortex reaches ${routes}.
144
117
  */
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;
118
+ const app = new Hono();
119
+ app.route("/cortex", server);
120
+ ${datasets ? `app.route("/datasets", datasets);\n` : ""}
121
+ export default { port: ${SERVER_PORT}, fetch: app.fetch };
170
122
  `;
171
123
  }
172
124
 
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.
125
+ function manifestTs({ name, server, datasets }: Project) {
126
+ const serverUrl = `
127
+ /** Where Cortex reaches this backend, from .env. */
128
+ const serverUrl = process.env["CORTEX_SERVER_URL"];
129
+ if (!serverUrl) throw new Error("Set CORTEX_SERVER_URL in .env before applying this manifest.");
130
+ `;
131
+ const datasetHosts = `
132
+ // The orders this backend serves (datasets.ts), for every agent on its server.
133
+ datasetHosts: [
134
+ {
135
+ name: "store",
136
+ url: \`\${serverUrl}/datasets\`,
137
+ credentials: [],
138
+ expose: ["orders"],
139
+ pinned: ["orders"],
140
+ audience: { server: ${JSON.stringify(server)} },
141
+ },
142
+ ],`;
143
+
144
+ return `import { defineManifest } from "@m6d/cortex";
145
+
146
+ /*
147
+ * What this project declares in Cortex, which \`cortex apply\` makes Cortex
148
+ * match. The agent runs on the server registered as ${JSON.stringify(server)}: this
149
+ * backend, whose variable its prompt reads and whose native tool it calls.
176
150
  */
177
- function hasServices(database: Database, features: Feature[]) {
178
- return (
179
- database === "postgres" ||
180
- features.some((feature) => FEATURES[feature].compose !== undefined)
181
- );
151
+ ${datasets ? serverUrl : ""}
152
+ export default defineManifest({
153
+ id: ${JSON.stringify(manifestIdOf(name))},
154
+ agents: [
155
+ {
156
+ slug: "${AGENT_SLUG}",
157
+ name: "Helper",
158
+ server: ${JSON.stringify(server)},
159
+ prompt: "You are a helpful assistant. Address the user as {{userName}}.",
160
+ // No identity provider yet: every caller is one shared identity.
161
+ auth: { kind: "anonymous" },
162
+ },
163
+ ],${datasets ? datasetHosts : ""}
164
+ });
165
+ `;
182
166
  }
183
167
 
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.
168
+ /** The backend's settings, which setup.ts copies into .env, generating the key. */
169
+ const BACKEND_ENV = `# This backend's settings. Bun loads .env from the directory you run in, into
170
+ # the backend (\`bun run dev\`) and into \`cortex apply\`. Cortex's own settings
171
+ # are in cortex.env, which only docker compose reads.
200
172
  #
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("")}`;
213
- }
173
+ # This is the committed copy. \`bun install\` writes .env from it, generating
174
+ # the values it leaves blank, and never changes a value .env already holds.
175
+
176
+ # The Compose project this checkout's Cortex stack runs as, drawn for each
177
+ # checkout, so a clone never shares another checkout's containers or volumes.
178
+ COMPOSE_PROJECT_NAME=
179
+
180
+ # The key Cortex presents on every call to the server. Store the same value in
181
+ # Cortex as the secret the server is registered with.
182
+ CORTEX_SERVER_KEY=
183
+ # Where Cortex reaches this backend from inside its containers.
184
+ CORTEX_SERVER_URL=${SERVER_URL}
185
+ `;
214
186
 
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[] = [];
187
+ /** Cortex's settings, which setup.ts copies into cortex.env, generating the secrets. */
188
+ const CORTEX_ENV = `# Cortex's settings, which docker compose hands the Cortex containers in
189
+ # docker-compose.yml. The backend never loads cortex.env.
190
+ #
191
+ # This is the committed copy. \`bun install\` writes cortex.env from it,
192
+ # generating the secrets it leaves blank, and never changes a value
193
+ # cortex.env already holds.
194
+
195
+ # Cortex's first admin, created when the stack first starts, and
196
+ # the management key \`cortex login\` takes.
197
+ DEFAULT_ADMIN_EMAIL=admin@example.com
198
+ DEFAULT_ADMIN_PASSWORD=
199
+ BOOTSTRAP_MANAGEMENT_KEY=
200
+ # Session signing, and the key the secrets store is encrypted with.
201
+ BETTER_AUTH_SECRET=
202
+ SECRETS_MASTER_KEY=
203
+ # Any OpenAI-compatible embeddings endpoint and model. Tool search and
204
+ # knowledge collections embed with it.
205
+ EMBEDDING_BASE_URL=https://api.openai.com/v1
206
+ EMBEDDING_API_KEY=your-embedding-api-key
207
+ EMBEDDING_MODEL=text-embedding-3-small
208
+ `;
224
209
 
225
- if (database === "postgres") {
226
- services.push(` postgres:
227
- image: postgres:17-alpine
210
+ /** Cortex itself: its app, worker and engine, all on the one Cortex image, and their stores. */
211
+ function dockerCompose() {
212
+ return `# Cortex, for this project's agents to run on: the console at
213
+ # http://localhost:{{port.cortex}}, the engine widgets and trigger callers reach at
214
+ # http://localhost:{{port.engine}}, and the stores they keep state in. \`docker compose
215
+ # up -d\` starts it; cortex.env holds its secrets.
216
+ #
217
+ # The two host ports were picked at scaffold time from what was free on this
218
+ # machine. Move one and the URLs that name it here and in README.md follow.
219
+ # CORTEX_VERSION, set in the shell, runs another release of the image.
220
+ #
221
+ # The console's browser uploads (knowledge documents) go straight to
222
+ # S3_ENDPOINT, which only this stack resolves. Point it at storage the
223
+ # browser reaches too before relying on them.
224
+
225
+ # The stack runs as .env's COMPOSE_PROJECT_NAME, drawn for each checkout by
226
+ # setup.ts, so no two checkouts share containers or volumes, whatever their
227
+ # directories are called.
228
+
229
+ x-cortex: &cortex
230
+ image: syscodeia/cortex:\${CORTEX_VERSION:-${CORTEX_VERSION}}
231
+ working_dir: /repo/apps/cortex
232
+ env_file: cortex.env
233
+ environment: &cortex-environment
234
+ DATABASE_URL: postgres://postgres:postgres@postgres:5432/cortex
235
+ REDIS_URL: redis://redis:6379
236
+ S3_ENDPOINT: http://minio:9000
237
+ S3_REGION: us-east-1
238
+ S3_ACCESS_KEY_ID: minio
239
+ S3_SECRET_ACCESS_KEY: minio-secret-key
240
+ S3_BUCKET: cortex
241
+ BETTER_AUTH_ALLOWED_HOSTS: localhost:{{port.cortex}},127.0.0.1:{{port.cortex}}
242
+ # The dimension the image's migrations give the vector columns.
243
+ EMBEDDING_DIMENSION: "768"
244
+ # The engine's public origin (browsers, trigger callers) and the
245
+ # address the app reaches it at inside the stack.
246
+ ENGINE_PUBLIC_URL: http://localhost:{{port.engine}}
247
+ ENGINE_INTERNAL_URL: http://engine:3001
248
+ # This backend is a private address seen from the containers, and the
249
+ # server key travels to it over plain http. The policy admits a host
250
+ # by name, whatever the port: see README.md.
251
+ EGRESS_ALLOWED_HOSTS: host.docker.internal
252
+ # Docker Desktop resolves host.docker.internal on its own; Linux needs this.
253
+ extra_hosts:
254
+ - host.docker.internal:host-gateway
255
+
256
+ services:
257
+ postgres:
258
+ image: pgvector/pgvector:pg17
259
+ restart: unless-stopped
228
260
  environment:
229
261
  POSTGRES_USER: postgres
230
262
  POSTGRES_PASSWORD: postgres
231
263
  POSTGRES_DB: cortex
232
- ports:
233
- - "{{port.postgres}}:5432"
234
264
  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}`;
265
+ - postgres-data:/var/lib/postgresql/data
266
+ healthcheck:
267
+ test: ["CMD-SHELL", "pg_isready -U postgres -d cortex"]
268
+ interval: 5s
269
+ timeout: 5s
270
+ retries: 10
271
+
272
+ redis:
273
+ image: redis:7-alpine
274
+ restart: unless-stopped
275
+ healthcheck:
276
+ test: ["CMD", "redis-cli", "ping"]
277
+ interval: 5s
278
+ timeout: 5s
279
+ retries: 10
280
+
281
+ minio:
282
+ # A community rebuild of MinIO's last free release, pinned by digest:
283
+ # MinIO's own images left Docker Hub. It runs as nobody, so it takes
284
+ # root to reuse a volume another image wrote.
285
+ image: sourcemation/minio@sha256:538065d2dd0ef360fe3f22f25d2384e6bae9075b6d2603cb5587193bd73e6276
286
+ user: "0:0"
287
+ restart: unless-stopped
288
+ command: server /data
289
+ environment:
290
+ MINIO_ROOT_USER: minio
291
+ MINIO_ROOT_PASSWORD: minio-secret-key
292
+ volumes:
293
+ - minio-data:/data
294
+ healthcheck:
295
+ test: ["CMD", "bash", "-c", "</dev/tcp/127.0.0.1/9000"]
296
+ interval: 5s
297
+ timeout: 5s
298
+ retries: 10
299
+
300
+ minio-init:
301
+ image: chainguard/minio-client@sha256:b2bd7824d23d3e3b15bedd7e87fbc3be29d2e213307b4f901e4a1d92356dc20f
302
+ depends_on:
303
+ minio:
304
+ condition: service_healthy
305
+ environment:
306
+ MC_HOST_local: http://minio:minio-secret-key@minio:9000
307
+ command: ["mb", "--ignore-existing", "local/cortex"]
308
+
309
+ # One-shot: migrate the database, then create the first admin and its key.
310
+ migrator:
311
+ <<: *cortex
312
+ command: ["bun", "run", "db:migrate"]
313
+ depends_on:
314
+ postgres:
315
+ condition: service_healthy
316
+
317
+ seeder:
318
+ <<: *cortex
319
+ command: ["bun", "run", "db:seed:admin"]
320
+ depends_on:
321
+ migrator:
322
+ condition: service_completed_successfully
323
+
324
+ # The console and the management API.
325
+ app:
326
+ <<: *cortex
327
+ restart: unless-stopped
328
+ ports:
329
+ - "127.0.0.1:{{port.cortex}}:3000"
330
+ depends_on:
331
+ seeder:
332
+ condition: service_completed_successfully
333
+ minio-init:
334
+ condition: service_completed_successfully
335
+
336
+ worker:
337
+ <<: *cortex
338
+ restart: unless-stopped
339
+ command: ["bun", "src/jobs/worker.ts"]
340
+ depends_on:
341
+ seeder:
342
+ condition: service_completed_successfully
343
+ minio-init:
344
+ condition: service_completed_successfully
345
+ redis:
346
+ condition: service_healthy
347
+
348
+ # Runs the agents: widgets, trigger callers and the console's playground
349
+ # and assistant talk to it.
350
+ engine:
351
+ <<: *cortex
352
+ restart: unless-stopped
353
+ command: ["bun", "src/engine/server.ts"]
354
+ environment:
355
+ <<: *cortex-environment
356
+ # Browser origins allowed to call the engine: the console's, for its playground and assistant.
357
+ ENGINE_CORS_ORIGINS: http://localhost:{{port.cortex}},http://127.0.0.1:{{port.cortex}}
358
+ ports:
359
+ - "127.0.0.1:{{port.engine}}:3001"
360
+ depends_on:
361
+ seeder:
362
+ condition: service_completed_successfully
363
+ redis:
364
+ condition: service_healthy
365
+
366
+ volumes:
367
+ postgres-data:
368
+ minio-data:
369
+ `;
268
370
  }
269
371
 
270
372
  function cspellConfig() {
@@ -300,7 +402,6 @@ function cspellConfig() {
300
402
  ".vercel/**",
301
403
  "vendor/**",
302
404
  ".gitignore",
303
- "**/domains/**/*.concept.ts",
304
405
  ],
305
406
  dictionaries: [
306
407
  "typescript",
@@ -318,41 +419,64 @@ function cspellConfig() {
318
419
  )}\n`;
319
420
  }
320
421
 
321
- function readme(name: string, database: Database, features: Feature[], compose: boolean) {
422
+ function readme({ name, server, datasets }: Project) {
322
423
  return `# ${name}
323
424
 
324
- A Cortex server: one agent, a ${database} database, and an OpenAI-compatible model.${
325
- features.length === 0 ? "" : ` Also on: ${features.join(", ")}.`
326
- }
425
+ A backend for Cortex agents, and Cortex itself to run them on.
426
+
427
+ - \`server.ts\` is this backend's Cortex server: the \`userName\` variable the agents' prompts read,
428
+ and the \`rollDice\` tool it runs for them.${
429
+ datasets
430
+ ? "\n- `datasets.ts` serves an `orders` table as a dataset host, for agents to ask grouped\n questions of."
431
+ : ""
432
+ }
433
+ - \`cortex.manifest.ts\` declares the \`${AGENT_SLUG}\` agent on this server${
434
+ datasets ? " and the dataset host" : ""
435
+ }. \`cortex apply\` makes Cortex match it.
436
+ - \`docker-compose.yml\` runs Cortex: its app, worker and engine, and the stores they use, with
437
+ their secrets in \`cortex.env\`. \`.env\` holds the backend's own key.
438
+ - \`setup.ts\` runs on every \`bun install\`: it writes \`.env\` and \`cortex.env\` from their committed
439
+ \`.example\` copies and generates each value they leave blank, so a fresh clone gets its own
440
+ secrets and its own Compose stack. It never changes a value already set. \`.dockerignore\` keeps
441
+ all four files out of any image built here.
327
442
 
328
443
  ## Getting started
329
444
 
330
- \`\`\`sh
331
- bun install
332
- ${compose ? "docker compose up -d\n" : ""}# fill in CORTEX_MODEL_* in .env
333
- bun run dev
334
- \`\`\`
445
+ 1. On a fresh clone, run \`bun install\` (\`cortex new\` already did). Then fill in
446
+ \`EMBEDDING_API_KEY\` in \`cortex.env\`.
447
+ 2. Start Cortex and this backend:
335
448
 
336
- The server listens on http://localhost:3331. Change \`port\` in \`cortex.config.ts\` to move it.${
337
- compose
338
- ? `
449
+ \`\`\`sh
450
+ docker compose up -d
451
+ bun run dev
452
+ \`\`\`
339
453
 
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
- }
454
+ 3. Open the console at http://localhost:{{port.cortex}} and sign in with
455
+ \`DEFAULT_ADMIN_EMAIL\` and \`DEFAULT_ADMIN_PASSWORD\` from \`cortex.env\`.
456
+ 4. Register the server. Add a model provider on the **Model providers** page and store
457
+ \`CORTEX_SERVER_KEY\` from \`.env\` on the **Secrets** page. Then on the **Servers** page, add a
458
+ server named \`${server}\` at \`${SERVER_URL}/cortex\` with that secret as its key, and pick
459
+ the model its agents run on.
460
+ 5. Apply the manifest. \`cortex login\` asks for a management key: \`BOOTSTRAP_MANAGEMENT_KEY\` from
461
+ \`cortex.env\`.
345
462
 
346
- ## Layout
463
+ \`\`\`sh
464
+ bun cortex login --url http://localhost:{{port.cortex}}
465
+ bun cortex apply
466
+ \`\`\`
347
467
 
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.
468
+ Then chat with \`${AGENT_SLUG}\` on the console's **Playground** page. Edit \`cortex.manifest.ts\` and
469
+ apply again to change it; an apply changes only what the edit changed.
354
470
 
355
- Commented-out sections in \`cortex.config.ts\` document what else the server can do.
471
+ The backend listens on http://localhost:${SERVER_PORT}. Move it in \`index.ts\`, and
472
+ \`CORTEX_SERVER_URL\` and the server's URL in Cortex with it.
473
+
474
+ ## Reaching this machine
475
+
476
+ Cortex's containers reach the backend at \`host.docker.internal\`, which \`EGRESS_ALLOWED_HOSTS\` in
477
+ \`docker-compose.yml\` admits. The policy admits a host by name, not by port, so every HTTP tool, MCP
478
+ server and credential you define in Cortex can reach any port this machine listens
479
+ on, over plain http. Keep the stack on your own machine, and define only tools you trust.
356
480
 
357
481
  ## Checks
358
482
 
@@ -363,36 +487,37 @@ pre-commit hook or CI job should run. Each is also available on its own: \`forma
363
487
  }
364
488
 
365
489
  /**
366
- * Relative path → contents. Nothing here touches the filesystem. Every
367
- * generator lists features in catalogue order, whatever order they came in.
490
+ * Relative path → contents. Nothing here touches the filesystem; the ports
491
+ * are drawn by the caller.
368
492
  */
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
-
493
+ export function scaffoldFiles(input: Input) {
494
+ const project = { ...input, server: input.name.slice(0, SERVER_NAME_MAX) };
374
495
  const files = {
375
- "cortex.config.ts": cortexConfig(database, features),
376
- "index.ts": indexTs,
377
- "package.json": packageJson(name),
496
+ "server.ts": serverTs,
497
+ "setup.ts": setupTs,
498
+ ...(project.datasets ? { "datasets.ts": datasetsTs } : {}),
499
+ "index.ts": indexTs(project),
500
+ "cortex.manifest.ts": manifestTs(project),
501
+ "docker-compose.yml": dockerCompose(),
502
+ "package.json": packageJson(project),
378
503
  "tsconfig.json": tsconfigJson,
379
- ".env": envFile,
380
- ".env.example": envFile,
504
+ ".env.example": BACKEND_ENV,
505
+ "cortex.env.example": CORTEX_ENV,
381
506
  ".gitignore": gitignoreText,
382
- "README.md": readme(name, database, features, compose !== undefined),
507
+ ".dockerignore": dockerignoreText,
508
+ "README.md": readme(project),
383
509
  "eslint.config.mjs": eslintConfig,
384
510
  "prettier.config.mjs": prettierConfig,
385
511
  "cspell.json": cspellConfig(),
386
512
  ".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
513
  } satisfies Record<string, string>;
392
514
 
393
515
  // 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.
516
+ // its own, so compose and the README cannot disagree about one.
395
517
  return Object.fromEntries(
396
- Object.entries(files).map(([path, contents]) => [path, applyPorts(contents, ports)]),
518
+ Object.entries(files).map(([path, contents]) => [
519
+ path,
520
+ applyPorts(contents, project.ports),
521
+ ]),
397
522
  );
398
523
  }