@m6d/cortex-cli 0.13.0 → 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.
@@ -17,6 +17,9 @@ import dockerignoreText from "@/scaffold/templates/dockerignore.txt";
17
17
  import eslintConfig from "@/scaffold/templates/eslint.config.mjs.txt";
18
18
  import prettierConfig from "@/scaffold/templates/prettier.config.mjs.txt";
19
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";
20
23
  import { applyPorts, type Ports } from "@/scaffold/ports";
21
24
 
22
25
  /**
@@ -24,11 +27,13 @@ import { applyPorts, type Ports } from "@/scaffold/ports";
24
27
  * `@m6d/cortex` range below are stamped by scripts/sync-package-versions.ts
25
28
  * before the CLI publishes, so a scaffold pins what was current then.
26
29
  */
27
- const CORTEX_VERSION = "0.13.0";
30
+ const CORTEX_VERSION = "0.14.1";
28
31
 
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
+ /** 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}}`;
32
37
 
33
38
  /** The longest server name Cortex accepts. */
34
39
  const SERVER_NAME_MAX = 100;
@@ -37,27 +42,76 @@ const SERVER_NAME_MAX = 100;
37
42
  * The agent the manifest declares and the README sends you to chat with.
38
43
  * Not `assistant`: Cortex holds that slug for its console's own assistant.
39
44
  */
40
- const AGENT_SLUG = "helper";
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;
41
53
 
42
54
  const DEV_DEPENDENCIES = {
43
- // `cortex apply` and the management commands, at the release that wrote the project.
55
+ // `cortex apply`, `cortex setup` and the management commands, at the release that wrote the project.
44
56
  "@m6d/cortex-cli": `~${pkg.version}`,
45
57
  "@types/bun": "latest",
58
+ // The two runners of `bun run dev` and `bun run dev:agent`.
59
+ concurrently: "^10.0.6",
46
60
  cspell: "^9.6.4",
47
61
  eslint: "^9",
48
62
  "eslint-config-prettier": "^10.1.8",
49
63
  "eslint-plugin-prettier": "^5.5.5",
50
64
  fallow: "^3.9.1",
65
+ mprocs: "^0.9.6",
51
66
  prettier: "^3.6.2",
52
67
  typescript: "^5",
53
68
  "typescript-eslint": "^8.65.0",
54
69
  };
55
70
 
56
- type Input = { name: string; datasets: boolean; ports: Ports };
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
+ };
57
83
 
58
84
  /** The input, and the name the backend is registered under in Cortex. */
59
85
  type Project = Input & { server: string };
60
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
+
61
115
  /** The name as the manifest `id` takes it: lowercase words and hyphens. */
62
116
  export function manifestIdOf(name: string) {
63
117
  return name
@@ -66,7 +120,9 @@ export function manifestIdOf(name: string) {
66
120
  .replace(/^-+|-+$/g, "");
67
121
  }
68
122
 
69
- function packageJson({ name, datasets }: Project) {
123
+ function packageJson({ name, datasets, setup }: Project) {
124
+ const processes = devProcesses(setup);
125
+
70
126
  return `${JSON.stringify(
71
127
  {
72
128
  name,
@@ -80,7 +136,11 @@ function packageJson({ name, datasets }: Project) {
80
136
  // analyses — dead code, duplication, complexity — over everything.
81
137
  audit: "fallow",
82
138
  check: "tsc --noEmit",
83
- 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",
84
144
  // A no-op where setup.ts is not copied in, as in an image build.
85
145
  postinstall: "bun run --if-present setup.ts",
86
146
  format: "prettier --write .",
@@ -90,7 +150,7 @@ function packageJson({ name, datasets }: Project) {
90
150
  verify: "bun run format:check && bun run lint && bun run spellcheck && bun run check && bun run audit",
91
151
  },
92
152
  dependencies: {
93
- "@m6d/cortex": "~0.1.0",
153
+ "@m6d/cortex": "~0.2.0",
94
154
  // drizzle 1.0 is in beta; this is the build `@m6d/cortex/drizzle` is tested against.
95
155
  ...(datasets ? { "drizzle-orm": "1.0.0-beta.15-859cf75" } : {}),
96
156
  hono: "^4.12.3",
@@ -118,16 +178,20 @@ ${datasets ? `import { datasets } from "./datasets";\n` : ""}import { server } f
118
178
  const app = new Hono();
119
179
  app.route("/cortex", server);
120
180
  ${datasets ? `app.route("/datasets", datasets);\n` : ""}
121
- export default { port: ${SERVER_PORT}, fetch: app.fetch };
181
+ export default { port: {{port.server}}, fetch: app.fetch };
122
182
  `;
123
183
  }
124
184
 
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
- `;
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.");`;
131
195
  const datasetHosts = `
132
196
  // The orders this backend serves (datasets.ts), for every agent on its server.
133
197
  datasetHosts: [
@@ -137,7 +201,7 @@ if (!serverUrl) throw new Error("Set CORTEX_SERVER_URL in .env before applying t
137
201
  credentials: [],
138
202
  expose: ["orders"],
139
203
  pinned: ["orders"],
140
- audience: { server: ${JSON.stringify(server)} },
204
+ audience: { server },
141
205
  },
142
206
  ],`;
143
207
 
@@ -145,17 +209,19 @@ if (!serverUrl) throw new Error("Set CORTEX_SERVER_URL in .env before applying t
145
209
 
146
210
  /*
147
211
  * 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.
212
+ * match. The agent runs on this backend's server, whose variable its prompt
213
+ * reads and whose native tool it calls.
150
214
  */
151
- ${datasets ? serverUrl : ""}
215
+
216
+ ${fromEnv}
217
+
152
218
  export default defineManifest({
153
219
  id: ${JSON.stringify(manifestIdOf(name))},
154
220
  agents: [
155
221
  {
156
222
  slug: "${AGENT_SLUG}",
157
223
  name: "Helper",
158
- server: ${JSON.stringify(server)},
224
+ server,
159
225
  prompt: "You are a helpful assistant. Address the user as {{userName}}.",
160
226
  // No identity provider yet: every caller is one shared identity.
161
227
  auth: { kind: "anonymous" },
@@ -165,35 +231,21 @@ export default defineManifest({
165
231
  `;
166
232
  }
167
233
 
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.
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.
172
239
  #
173
240
  # 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.
241
+ # the secrets it leaves blank, and never changes a value .env already holds.
175
242
 
176
243
  # The Compose project this checkout's Cortex stack runs as, drawn for each
177
244
  # checkout, so a clone never shares another checkout's containers or volumes.
178
245
  COMPOSE_PROJECT_NAME=
179
246
 
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
- `;
186
-
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.
247
+ # Cortex's first admin, created when the stack first starts, and its
248
+ # management key.
197
249
  DEFAULT_ADMIN_EMAIL=admin@example.com
198
250
  DEFAULT_ADMIN_PASSWORD=
199
251
  BOOTSTRAP_MANAGEMENT_KEY=
@@ -201,21 +253,47 @@ BOOTSTRAP_MANAGEMENT_KEY=
201
253
  BETTER_AUTH_SECRET=
202
254
  SECRETS_MASTER_KEY=
203
255
  # Any OpenAI-compatible embeddings endpoint and model. Tool search and
204
- # knowledge collections embed with it.
256
+ # knowledge collections embed with it, and stay off while the key is blank.
205
257
  EMBEDDING_BASE_URL=https://api.openai.com/v1
206
- EMBEDDING_API_KEY=your-embedding-api-key
207
- EMBEDDING_MODEL=text-embedding-3-small
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=
208
284
  `;
285
+ }
209
286
 
210
287
  /** Cortex itself: its app, worker and engine, all on the one Cortex image, and their stores. */
211
288
  function dockerCompose() {
212
289
  return `# Cortex, for this project's agents to run on: the console at
213
290
  # 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.
291
+ # http://localhost:{{port.engine}}, and the stores they keep state in. \`bun run dev\`
292
+ # starts it; .env holds its secrets.
216
293
  #
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.
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.
219
297
  # CORTEX_VERSION, set in the shell, runs another release of the image.
220
298
  #
221
299
  # The console's browser uploads (knowledge documents) go straight to
@@ -229,8 +307,12 @@ function dockerCompose() {
229
307
  x-cortex: &cortex
230
308
  image: syscodeia/cortex:\${CORTEX_VERSION:-${CORTEX_VERSION}}
231
309
  working_dir: /repo/apps/cortex
232
- env_file: cortex.env
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
233
313
  environment: &cortex-environment
314
+ NODE_ENV: production
315
+ PORT: "3000"
234
316
  DATABASE_URL: postgres://postgres:postgres@postgres:5432/cortex
235
317
  REDIS_URL: redis://redis:6379
236
318
  S3_ENDPOINT: http://minio:9000
@@ -240,15 +322,11 @@ x-cortex: &cortex
240
322
  S3_BUCKET: cortex
241
323
  BETTER_AUTH_ALLOWED_HOSTS: localhost:{{port.cortex}},127.0.0.1:{{port.cortex}}
242
324
  # The dimension the image's migrations give the vector columns.
243
- EMBEDDING_DIMENSION: "768"
325
+ EMBEDDING_DIMENSION: "${EMBEDDING.dimension}"
244
326
  # The engine's public origin (browsers, trigger callers) and the
245
327
  # address the app reaches it at inside the stack.
246
328
  ENGINE_PUBLIC_URL: http://localhost:{{port.engine}}
247
329
  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
330
  # Docker Desktop resolves host.docker.internal on its own; Linux needs this.
253
331
  extra_hosts:
254
332
  - host.docker.internal:host-gateway
@@ -353,6 +431,7 @@ services:
353
431
  command: ["bun", "src/engine/server.ts"]
354
432
  environment:
355
433
  <<: *cortex-environment
434
+ PORT: "3001"
356
435
  # Browser origins allowed to call the engine: the console's, for its playground and assistant.
357
436
  ENGINE_CORS_ORIGINS: http://localhost:{{port.cortex}},http://127.0.0.1:{{port.cortex}}
358
437
  ports:
@@ -369,57 +448,27 @@ volumes:
369
448
  `;
370
449
  }
371
450
 
372
- function cspellConfig() {
373
- return `${JSON.stringify(
374
- {
375
- version: "0.2",
376
- language: "en",
377
- // Yours to fill in. Project jargon belongs here; nothing is seeded,
378
- // because a stranger's vocabulary is noise in your repo.
379
- words: [],
380
- ignorePaths: [
381
- "node_modules/**",
382
- "out/**",
383
- "build/**",
384
- "**/dist/**",
385
- "coverage/**",
386
- "*.tsbuildinfo",
387
- "bun.lock",
388
- "package-lock.json",
389
- "pnpm-lock.yaml",
390
- "yarn.lock",
391
- "*.db",
392
- "*.sqlite",
393
- "scratch.*",
394
- ".env*",
395
- "**/*.min.js",
396
- "**/*.min.css",
397
- "**/db/migrations/**",
398
- "**/i18n/**",
399
- "docs",
400
- ".claude",
401
- ".angular",
402
- ".vercel/**",
403
- "vendor/**",
404
- ".gitignore",
405
- ],
406
- dictionaries: [
407
- "typescript",
408
- "node",
409
- "npm",
410
- "html",
411
- "css",
412
- "bash",
413
- "companies",
414
- "softwareTerms",
415
- ],
416
- },
417
- null,
418
- 4,
419
- )}\n`;
420
- }
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\`.`;
421
471
 
422
- function readme({ name, server, datasets }: Project) {
423
472
  return `# ${name}
424
473
 
425
474
  A backend for Cortex agents, and Cortex itself to run them on.
@@ -433,48 +482,38 @@ A backend for Cortex agents, and Cortex itself to run them on.
433
482
  - \`cortex.manifest.ts\` declares the \`${AGENT_SLUG}\` agent on this server${
434
483
  datasets ? " and the dataset host" : ""
435
484
  }. \`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.
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.
442
491
 
443
492
  ## Getting started
444
493
 
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:
448
-
449
- \`\`\`sh
450
- docker compose up -d
451
- bun run dev
452
- \`\`\`
453
-
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\`.
462
-
463
- \`\`\`sh
464
- bun cortex login --url http://localhost:{{port.cortex}}
465
- bun cortex apply
466
- \`\`\`
467
-
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.
470
-
471
- The backend listens on http://localhost:${SERVER_PORT}. Move it in \`index.ts\`, and
494
+ On a fresh clone, run \`bun install\` first (\`cortex new\` already did): it writes \`.env\`.
495
+
496
+ \`\`\`sh
497
+ bun run dev
498
+ \`\`\`
499
+
500
+ ${gettingStarted}
501
+
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.
506
+
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.
509
+
510
+ The backend listens on http://localhost:{{port.server}}. Move it in \`index.ts\`, and
472
511
  \`CORTEX_SERVER_URL\` and the server's URL in Cortex with it.
473
512
 
474
513
  ## Reaching this machine
475
514
 
476
515
  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
516
+ \`.env\` admits. The policy admits a host by name, not by port, so every HTTP tool, MCP
478
517
  server and credential you define in Cortex can reach any port this machine listens
479
518
  on, over plain http. Keep the stack on your own machine, and define only tools you trust.
480
519
 
@@ -501,14 +540,14 @@ export function scaffoldFiles(input: Input) {
501
540
  "docker-compose.yml": dockerCompose(),
502
541
  "package.json": packageJson(project),
503
542
  "tsconfig.json": tsconfigJson,
504
- ".env.example": BACKEND_ENV,
505
- "cortex.env.example": CORTEX_ENV,
543
+ ".env.example": envExample(project),
544
+ ...(project.env ? { ".env": withEnvValues(envExample(project), project.env) } : {}),
506
545
  ".gitignore": gitignoreText,
507
546
  ".dockerignore": dockerignoreText,
508
547
  "README.md": readme(project),
509
548
  "eslint.config.mjs": eslintConfig,
510
549
  "prettier.config.mjs": prettierConfig,
511
- "cspell.json": cspellConfig(),
550
+ "cspell.json": cspellJson,
512
551
  ".fallowrc.jsonc": fallowrc,
513
552
  } satisfies Record<string, string>;
514
553
 
@@ -4,10 +4,9 @@
4
4
  *
5
5
  * Only the app and the engine are published: the browser and this
6
6
  * project's callers reach them, and nothing outside the stack reaches its
7
- * databases. A fixed pair would collide with the next project scaffolded on
8
- * the same machine, so each project draws its own. Container ports stay
9
- * canonical. The backend's own port is not one of these: it is the project's
10
- * to choose, and stays the documented default.
7
+ * databases. The backend listens on the third. Fixed ports would collide with
8
+ * the next project scaffolded on the same machine, so each project draws its
9
+ * own. Container ports stay canonical.
11
10
  *
12
11
  * Every generated file spells its ports `{{port.<name>}}` and gets them filled in
13
12
  * by `applyPorts`. That is the point of the token: compose and the README
@@ -61,12 +60,12 @@ export function allocatePorts() {
61
60
  return port;
62
61
  }
63
62
 
64
- return { cortex: next(), engine: next() };
63
+ return { cortex: next(), engine: next(), server: next() };
65
64
  }
66
65
 
67
66
  export type Ports = ReturnType<typeof allocatePorts>;
68
67
 
69
- /** Fills in `{{port.cortex}}` and `{{port.engine}}`. Applied to every file, once, at the end. */
68
+ /** Fills in every `{{port.<name>}}`. Applied to every file, once, at the end. */
70
69
  export function applyPorts(contents: string, ports: Ports) {
71
70
  return Object.entries(ports).reduce(
72
71
  (text, [name, port]) => text.replaceAll(`{{port.${name}}}`, String(port)),
@@ -0,0 +1,41 @@
1
+ {
2
+ "version": "0.2",
3
+ "language": "en",
4
+ "words": ["mprocs"],
5
+ "ignorePaths": [
6
+ "node_modules/**",
7
+ "out/**",
8
+ "build/**",
9
+ "**/dist/**",
10
+ "coverage/**",
11
+ "*.tsbuildinfo",
12
+ "bun.lock",
13
+ "package-lock.json",
14
+ "pnpm-lock.yaml",
15
+ "yarn.lock",
16
+ "*.db",
17
+ "*.sqlite",
18
+ "scratch.*",
19
+ ".env*",
20
+ "**/*.min.js",
21
+ "**/*.min.css",
22
+ "**/db/migrations/**",
23
+ "**/i18n/**",
24
+ "docs",
25
+ ".claude",
26
+ ".angular",
27
+ ".vercel/**",
28
+ "vendor/**",
29
+ ".gitignore"
30
+ ],
31
+ "dictionaries": [
32
+ "typescript",
33
+ "node",
34
+ "npm",
35
+ "html",
36
+ "css",
37
+ "bash",
38
+ "companies",
39
+ "softwareTerms"
40
+ ]
41
+ }
@@ -1,7 +1,6 @@
1
- # The env files never go into an image: they hold this checkout's secrets.
2
- # With their example copies left out too, setup.ts writes none during the
3
- # image's own `bun install`.
1
+ # .env never goes into an image: it holds this checkout's secrets. With
2
+ # .env.example left out too, setup.ts writes none during the image's own
3
+ # `bun install`.
4
4
  .env*
5
- cortex.env*
6
5
  node_modules/
7
6
  .git/
@@ -1,9 +1,8 @@
1
1
  # Dependencies
2
2
  node_modules/
3
3
 
4
- # Environment variables: the .example files are the committed ones
4
+ # Environment variables: .env.example is the committed one
5
5
  .env
6
- cortex.env
7
6
  .env.local
8
7
  .env.*.local
9
8