@m6d/cortex-cli 0.12.0 → 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.
- package/README.md +135 -130
- package/package.json +16 -5
- package/src/cli.ts +17 -12
- package/src/commands/apply.ts +169 -0
- package/src/commands/{cc.ts → management.ts} +150 -100
- package/src/commands/new.ts +50 -74
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +932 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +247 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +61 -32
- package/src/{cc → management}/client.ts +19 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +5 -5
- package/src/scaffold/files.ts +362 -237
- package/src/scaffold/ports.ts +13 -24
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +7 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +2 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +52 -0
- package/src/ui/prompts.ts +4 -31
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -45
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /package/src/{cc → management}/output.ts +0 -0
package/src/scaffold/files.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
11
|
-
import
|
|
12
|
-
import
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
${datasets ? `import { datasets } from "./datasets";\n` : ""}import { server } from "./server";
|
|
139
114
|
|
|
140
115
|
/**
|
|
141
|
-
*
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
#
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
#
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
#
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
#
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
422
|
+
function readme({ name, server, datasets }: Project) {
|
|
322
423
|
return `# ${name}
|
|
323
424
|
|
|
324
|
-
A
|
|
325
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
449
|
+
\`\`\`sh
|
|
450
|
+
docker compose up -d
|
|
451
|
+
bun run dev
|
|
452
|
+
\`\`\`
|
|
339
453
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
463
|
+
\`\`\`sh
|
|
464
|
+
bun cortex login --url http://localhost:{{port.cortex}}
|
|
465
|
+
bun cortex apply
|
|
466
|
+
\`\`\`
|
|
347
467
|
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
*
|
|
490
|
+
* Relative path → contents. Nothing here touches the filesystem; the ports
|
|
491
|
+
* are drawn by the caller.
|
|
368
492
|
*/
|
|
369
|
-
export function scaffoldFiles(
|
|
370
|
-
const
|
|
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
|
-
"
|
|
376
|
-
"
|
|
377
|
-
"
|
|
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":
|
|
380
|
-
".env.example":
|
|
504
|
+
".env.example": BACKEND_ENV,
|
|
505
|
+
"cortex.env.example": CORTEX_ENV,
|
|
381
506
|
".gitignore": gitignoreText,
|
|
382
|
-
"
|
|
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
|
|
516
|
+
// its own, so compose and the README cannot disagree about one.
|
|
395
517
|
return Object.fromEntries(
|
|
396
|
-
Object.entries(files).map(([path, contents]) => [
|
|
518
|
+
Object.entries(files).map(([path, contents]) => [
|
|
519
|
+
path,
|
|
520
|
+
applyPorts(contents, project.ports),
|
|
521
|
+
]),
|
|
397
522
|
);
|
|
398
523
|
}
|