@m6d/cortex-cli 0.13.0 → 0.15.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 +94 -26
- package/package.json +1 -1
- package/src/cli.ts +2 -0
- package/src/commands/apply.ts +171 -44
- package/src/commands/management.ts +41 -22
- package/src/commands/new.ts +48 -25
- package/src/commands/setup.ts +611 -0
- package/src/contracts/datasets/dataset-sql.ts +22 -5
- package/src/contracts/datasets/datasets.ts +139 -16
- package/src/contracts/egress-policy/scrub.ts +11 -8
- package/src/contracts/json/index.ts +21 -0
- package/src/contracts/manifest/index.ts +223 -55
- package/src/contracts/server/index.ts +109 -9
- package/src/contracts/server/reserved-names.fixtures.json +1 -3
- package/src/contracts/wire/index.ts +42 -3
- package/src/management/client.ts +14 -0
- package/src/management/profiles.ts +6 -4
- package/src/scaffold/files.ts +183 -144
- package/src/scaffold/ports.ts +5 -6
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/dockerignore.txt +3 -4
- package/src/scaffold/templates/gitignore.txt +1 -2
- package/src/scaffold/templates/setup.ts.txt +18 -26
- package/src/ui/prompts.ts +32 -10
|
@@ -131,12 +131,23 @@ export type ClientToolBinding = {
|
|
|
131
131
|
*/
|
|
132
132
|
export type AgentCapabilities = {
|
|
133
133
|
attachments: boolean;
|
|
134
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* False when this caller may not send to the chat: the agent's `message`
|
|
136
|
+
* trigger is off, or the caller only reads (`readOnly`). The widget hides
|
|
137
|
+
* its composer and answers no tool call.
|
|
138
|
+
*/
|
|
135
139
|
messages: boolean;
|
|
140
|
+
/** True when the caller only reads, as an operator inspecting a user's thread does. */
|
|
141
|
+
readOnly: boolean;
|
|
136
142
|
/** Prompt chips on the blank-chat landing per locale; clicking one sends its `prompt` as the first message. */
|
|
137
143
|
suggestions: Suggestions;
|
|
138
144
|
/** Headline of the blank-chat landing per locale; "" falls back to the widget's built-in copy. */
|
|
139
145
|
greeting: Greeting;
|
|
146
|
+
/**
|
|
147
|
+
* Whether this caller gets the debug view: tool inputs and outputs, reasoning,
|
|
148
|
+
* model calls. Cortex sends anyone else only what the widget's normal UI draws.
|
|
149
|
+
*/
|
|
150
|
+
debug: boolean;
|
|
140
151
|
};
|
|
141
152
|
|
|
142
153
|
export type MessageMetadata = {
|
|
@@ -171,13 +182,25 @@ export type LocalizedText = Record<Locale, string>;
|
|
|
171
182
|
*/
|
|
172
183
|
export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"] as const;
|
|
173
184
|
|
|
185
|
+
/**
|
|
186
|
+
* The input Cortex adds to every tool that waits for the end user's approval:
|
|
187
|
+
* the model writes what that one call will do, and the approval card shows it
|
|
188
|
+
* as its title. Cortex removes it before the call runs.
|
|
189
|
+
*/
|
|
190
|
+
export const APPROVAL_TITLE_FIELD = "approvalTitle";
|
|
191
|
+
|
|
192
|
+
/** The title the model wrote into an approval call's input, or undefined while it has none. */
|
|
193
|
+
export function approvalTitleOf(input: unknown) {
|
|
194
|
+
if (!input || typeof input !== "object" || !(APPROVAL_TITLE_FIELD in input)) return undefined;
|
|
195
|
+
const title: unknown = input[APPROVAL_TITLE_FIELD];
|
|
196
|
+
return typeof title === "string" && title.trim() ? title.trim() : undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
174
199
|
/**
|
|
175
200
|
* What Cortex stamps on a tool call's `metadata.cortex` from its first chunk,
|
|
176
201
|
* so the live part and the stored one agree.
|
|
177
202
|
*/
|
|
178
203
|
export type ToolCallMetadata = {
|
|
179
|
-
/** The action the approval card names, in the author's words rather than the function's name. */
|
|
180
|
-
approval?: LocalizedText;
|
|
181
204
|
/**
|
|
182
205
|
* Set on every call the client answers: a client tool's, the consumer's own, or a built-in
|
|
183
206
|
* input's. Cortex runs every other call, so only these take an output from a request.
|
|
@@ -326,3 +349,19 @@ export type WsEvent =
|
|
|
326
349
|
|
|
327
350
|
/** The header the widget sends the browser's IANA time zone in; the turn reasons in that clock. */
|
|
328
351
|
export const TIMEZONE_HEADER = "X-Cortex-Timezone";
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The console operator's short-lived token, sent by the playground and the
|
|
355
|
+
* thread inspector on every request to an agent and by the fire and answer
|
|
356
|
+
* calls Cortex makes to a trigger's route. A verified one makes the operator the caller unless the
|
|
357
|
+
* request also carries an end-user token to test as, and marks the threads it
|
|
358
|
+
* opens as test threads. A WebSocket upgrade carries it as `?operator=`.
|
|
359
|
+
*/
|
|
360
|
+
export const OPERATOR_HEADER = "X-Cortex-Operator";
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Beside the operator token, the id of a thread the operator inspects: the
|
|
364
|
+
* request then reads as that thread's owner, read-only, with the debug view.
|
|
365
|
+
* A WebSocket upgrade carries it as `?inspect=`.
|
|
366
|
+
*/
|
|
367
|
+
export const INSPECT_HEADER = "X-Cortex-Inspect";
|
package/src/management/client.ts
CHANGED
|
@@ -225,6 +225,8 @@ export async function callManagement(
|
|
|
225
225
|
...(method === "post" ? { "Content-Type": "application/json" } : {}),
|
|
226
226
|
},
|
|
227
227
|
body: method === "post" && input !== undefined ? JSON.stringify(input) : undefined,
|
|
228
|
+
}).catch((error: unknown) => {
|
|
229
|
+
throw new UnreachableError(error);
|
|
228
230
|
});
|
|
229
231
|
const body: unknown = await response.json().catch(() => null);
|
|
230
232
|
if (!response.ok) {
|
|
@@ -261,6 +263,18 @@ function queryString(input: Record<string, unknown>) {
|
|
|
261
263
|
return encoded === "" ? "" : `?${encoded}`;
|
|
262
264
|
}
|
|
263
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Cortex did not answer at all: refused, reset, unresolvable. `fetch` rejects
|
|
268
|
+
* only for these, with whatever its runtime throws (Bun's is a plain `Error`
|
|
269
|
+
* with a `code`), so `callManagement` names them once here.
|
|
270
|
+
*/
|
|
271
|
+
export class UnreachableError extends Error {
|
|
272
|
+
public constructor(cause: unknown) {
|
|
273
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
274
|
+
this.name = "UnreachableError";
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
264
278
|
/** A refusal; `detail` is what the refusal names, when it names something (an entry, the row holding a name). */
|
|
265
279
|
export class ManagementError extends Error {
|
|
266
280
|
public constructor(
|
|
@@ -87,14 +87,16 @@ export function removeProfile(name: string) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
|
-
* The profile a management command runs against: `
|
|
91
|
-
* when both are set (CI, containers
|
|
92
|
-
* the
|
|
90
|
+
* The profile a management command runs against: the entry `name` picks from
|
|
91
|
+
* the file, else `CORTEX_URL` + `CORTEX_KEY` when both are set (CI, containers,
|
|
92
|
+
* a project's .env), else the current entry. A name always wins, so `-p prod`
|
|
93
|
+
* inside a project whose .env points at its own Cortex still reaches prod.
|
|
94
|
+
* `undefined` means nobody has logged in.
|
|
93
95
|
*/
|
|
94
96
|
export function resolveProfile(name?: string) {
|
|
95
97
|
const url = process.env["CORTEX_URL"];
|
|
96
98
|
const key = process.env["CORTEX_KEY"];
|
|
97
|
-
if (url && key) return { name: "env", url: url.replace(/\/+$/, ""), key };
|
|
99
|
+
if (name === undefined && url && key) return { name: "env", url: url.replace(/\/+$/, ""), key };
|
|
98
100
|
const store = readStore();
|
|
99
101
|
const chosen = name ?? store.current;
|
|
100
102
|
const profile = lookup(store, chosen);
|
package/src/scaffold/files.ts
CHANGED
|
@@ -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.
|
|
30
|
+
const CORTEX_VERSION = "0.15.0";
|
|
28
31
|
|
|
29
|
-
/**
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
+
/** This machine as Cortex's containers name it: the one private host the stack's egress policy admits. */
|
|
33
|
+
export const DOCKER_HOST = "host.docker.internal";
|
|
34
|
+
|
|
35
|
+
/** How Cortex's containers reach the backend on this machine, at the port drawn for it. */
|
|
36
|
+
const SERVER_URL = `http://${DOCKER_HOST}:{{port.server}}`;
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
153
|
+
"@m6d/cortex": "~0.3.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:
|
|
181
|
+
export default { port: {{port.server}}, fetch: app.fetch };
|
|
122
182
|
`;
|
|
123
183
|
}
|
|
124
184
|
|
|
125
|
-
function manifestTs({ name,
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
const serverUrl = process.env
|
|
129
|
-
if (!serverUrl)
|
|
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
|
|
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
|
|
149
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
#
|
|
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
|
|
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
|
-
#
|
|
181
|
-
#
|
|
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=
|
|
207
|
-
EMBEDDING_MODEL
|
|
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. \`
|
|
215
|
-
#
|
|
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
|
|
218
|
-
# machine. Move one and the URLs that name it here and in README.md
|
|
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
|
-
|
|
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: "
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
\`
|
|
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":
|
|
505
|
-
"
|
|
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":
|
|
550
|
+
"cspell.json": cspellJson,
|
|
512
551
|
".fallowrc.jsonc": fallowrc,
|
|
513
552
|
} satisfies Record<string, string>;
|
|
514
553
|
|
package/src/scaffold/ports.ts
CHANGED
|
@@ -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.
|
|
8
|
-
* the same machine, so each project draws its
|
|
9
|
-
*
|
|
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
|
|
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)),
|