@m6d/cortex-cli 0.19.0 → 0.21.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 +4 -4
- package/package.json +3 -3
- package/src/commands/new.ts +4 -4
- package/src/commands/setup.ts +19 -10
- package/src/contracts/datasets/dataset-sql.ts +1 -1
- package/src/contracts/management/index.ts +13 -1
- package/src/contracts/management/procedures.generated.ts +401 -216
- package/src/contracts/manifest/index.ts +130 -137
- package/src/contracts/server/index.ts +33 -9
- package/src/contracts/triggers/index.ts +9 -9
- package/src/contracts/wire/index.ts +11 -0
- package/src/management/client.ts +2 -0
- package/src/scaffold/files.ts +41 -37
- package/src/scaffold/ports.ts +5 -4
- package/src/scaffold/templates/cspell.json.txt +1 -1
- package/src/scaffold/templates/oxlintrc.json.txt +67 -0
- package/src/scaffold/templates/setup.ts.txt +1 -0
- package/src/scaffold/templates/eslint.config.mjs.txt +0 -20
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Scaffolds `./<name>`: a backend for Cortex agents, and Cortex itself to run them
|
|
|
38
38
|
- `bun run dev` starts Cortex (`docker compose up`), the backend and `bun cortex setup --wait --yes` in
|
|
39
39
|
an mprocs dashboard. `bun run dev:agent` runs the same through `concurrently`, one labelled log for
|
|
40
40
|
coding agents and CI.
|
|
41
|
-
-
|
|
41
|
+
- oxlint, prettier, cspell, fallow and tsconfig are inlined, so `bun run verify` works standalone.
|
|
42
42
|
|
|
43
43
|
It then runs `bun install`, and `git init` unless the target is already inside a work tree. An
|
|
44
44
|
existing non-empty directory is an error. So is a name like `cortex` or `cortex.`, since the
|
|
@@ -62,8 +62,8 @@ as `--yes`, so a missing name is a hard error rather than a hang.
|
|
|
62
62
|
|
|
63
63
|
The image runs at the Cortex release that was current when this CLI was published
|
|
64
64
|
(`CORTEX_VERSION` in the shell picks another); the backend depends on the `@m6d/cortex` release of
|
|
65
|
-
that time.
|
|
66
|
-
|
|
65
|
+
that time. The server is registered under the project name as the manifest `id` takes it:
|
|
66
|
+
lowercase words joined by hyphens, cut to Cortex's 64 characters.
|
|
67
67
|
|
|
68
68
|
## `cortex setup`
|
|
69
69
|
|
|
@@ -107,7 +107,7 @@ at and a new procedure needs no CLI release.
|
|
|
107
107
|
cortex login # asks for the URL and a management key
|
|
108
108
|
cortex status
|
|
109
109
|
cortex agents list
|
|
110
|
-
cortex agents create --
|
|
110
|
+
cortex agents create --name support --displayName '{"en":"Support"}' --serverId <uuid>
|
|
111
111
|
cortex tools create --json tool.json --publish
|
|
112
112
|
cortex tools list -o json | jq '.items[].name'
|
|
113
113
|
cortex logout
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m6d/cortex-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Scaffold Cortex projects and manage a Cortex deployment",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@cortex/contracts": "workspace:*",
|
|
32
|
-
"@types/bun": "1.
|
|
33
|
-
"typescript": "
|
|
32
|
+
"@types/bun": "1.4.4",
|
|
33
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
36
|
"access": "public"
|
package/src/commands/new.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { CORTEX_MANIFEST_ID } from "@cortex/contracts/manifest";
|
|
|
7
7
|
|
|
8
8
|
import pkg from "../../package.json";
|
|
9
9
|
import { askProvider } from "@/commands/setup";
|
|
10
|
-
import {
|
|
10
|
+
import { AGENT_NAME, manifestIdOf, scaffoldFiles } from "@/scaffold/files";
|
|
11
11
|
import { allocatePorts, type Ports } from "@/scaffold/ports";
|
|
12
12
|
import { askConfirm, askName, canAsk, intro, log, outro, runSteps } from "@/ui/prompts";
|
|
13
13
|
import * as report from "@/ui/report";
|
|
@@ -30,7 +30,7 @@ export function registerNew(program: Command<[], { verbose?: true }>) {
|
|
|
30
30
|
// way and `--yes` is not the only way to say "yes". Declaring the
|
|
31
31
|
// positive first leaves the value `undefined` when neither is passed,
|
|
32
32
|
// which is what marks the question as still unanswered.
|
|
33
|
-
.option("--setup", `Set up the ${
|
|
33
|
+
.option("--setup", `Set up the ${AGENT_NAME} agent on the first bun run dev`)
|
|
34
34
|
.option("--no-setup", "Leave Cortex empty; you register the backend yourself")
|
|
35
35
|
.option("--yes", "Accept the defaults instead of asking");
|
|
36
36
|
|
|
@@ -151,7 +151,7 @@ async function askSetup(flag: boolean | undefined, interactive: boolean) {
|
|
|
151
151
|
flag ??
|
|
152
152
|
(!interactive ||
|
|
153
153
|
(await askConfirm(
|
|
154
|
-
`Set up the ${
|
|
154
|
+
`Set up the ${AGENT_NAME} agent when you first run bun run dev?`,
|
|
155
155
|
true,
|
|
156
156
|
)));
|
|
157
157
|
const provider = setup && interactive ? await askProvider() : undefined;
|
|
@@ -178,7 +178,7 @@ function warnInstall(project: string, output: string, verbose: boolean) {
|
|
|
178
178
|
/** What the first `bun run dev` sets up: the agent, the server alone (no model yet), or nothing. */
|
|
179
179
|
function nextSteps(project: string, ports: Ports, setup: boolean, provider: boolean) {
|
|
180
180
|
const runs = {
|
|
181
|
-
agent: step("bun run dev", `→ sets up ${
|
|
181
|
+
agent: step("bun run dev", `→ sets up ${AGENT_NAME}, then prints where to chat`),
|
|
182
182
|
server:
|
|
183
183
|
step("bun run dev", "→ registers the server") +
|
|
184
184
|
step("bun cortex setup", "adds a model provider, then applies"),
|
package/src/commands/setup.ts
CHANGED
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
scrubCredentialStrings,
|
|
14
14
|
} from "@cortex/contracts/egress-policy";
|
|
15
15
|
import { ManagementClientError, managementPath } from "@cortex/contracts/management";
|
|
16
|
-
import { MODEL_PROVIDER_LIMITS,
|
|
16
|
+
import { MODEL_PROVIDER_LIMITS, SETTING_BOUNDS } from "@cortex/contracts/manifest";
|
|
17
|
+
import { displayNameSchema, NAME_MAX_LENGTH, nameSchema } from "@cortex/contracts/server";
|
|
17
18
|
import { z } from "zod";
|
|
18
19
|
|
|
19
20
|
import {
|
|
@@ -43,7 +44,7 @@ const POLL_MS = 2_000;
|
|
|
43
44
|
|
|
44
45
|
/** The backend as `cortex new` writes it into .env, which Bun loads from the directory it runs in. */
|
|
45
46
|
const projectSchema = z.object({
|
|
46
|
-
CORTEX_SERVER_NAME:
|
|
47
|
+
CORTEX_SERVER_NAME: nameSchema,
|
|
47
48
|
CORTEX_SERVER_URL: z.url(),
|
|
48
49
|
CORTEX_SERVER_KEY: z.string().min(1),
|
|
49
50
|
});
|
|
@@ -57,7 +58,7 @@ const ENV_SAFE = /^[^\s$#'"`\\]+$/;
|
|
|
57
58
|
|
|
58
59
|
/** The model provider in .env: all four values, or none. */
|
|
59
60
|
const providerFields = z.object({
|
|
60
|
-
MODEL_PROVIDER_NAME:
|
|
61
|
+
MODEL_PROVIDER_NAME: nameSchema,
|
|
61
62
|
MODEL_PROVIDER_URL: z
|
|
62
63
|
.string()
|
|
63
64
|
.max(MODEL_PROVIDER_LIMITS.urlLength)
|
|
@@ -80,12 +81,14 @@ type Profile = ReturnType<typeof requireProfile>;
|
|
|
80
81
|
|
|
81
82
|
const namedSchema = z.object({ id: z.string(), name: z.string() });
|
|
82
83
|
const providerRowSchema = namedSchema.extend({
|
|
84
|
+
displayName: displayNameSchema,
|
|
83
85
|
baseUrl: z.string(),
|
|
84
86
|
secretName: z.string(),
|
|
85
87
|
models: z.array(z.string()),
|
|
86
88
|
efforts: z.array(z.string()),
|
|
87
89
|
});
|
|
88
90
|
const serverSchema = namedSchema.extend({
|
|
91
|
+
displayName: displayNameSchema,
|
|
89
92
|
description: z.string().nullable(),
|
|
90
93
|
url: z.string().nullable(),
|
|
91
94
|
keySecretName: z.string().nullable(),
|
|
@@ -270,20 +273,23 @@ async function findByName<TItem extends typeof namedSchema>(
|
|
|
270
273
|
|
|
271
274
|
/**
|
|
272
275
|
* A secret's name for one value: its owner's name, what it is to the owner,
|
|
273
|
-
* and a short digest of the value, the owner's name cut
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
276
|
+
* and a short digest of the value, the owner's name cut, and stripped of the
|
|
277
|
+
* hyphens the cut leaves it ending in, so the whole is a name Cortex holds.
|
|
278
|
+
* A name then holds one value only, so a secret that exists is the value
|
|
279
|
+
* .env holds, and a changed key gets a secret of its own while every row
|
|
280
|
+
* presenting the old one keeps it.
|
|
277
281
|
*/
|
|
278
282
|
function secretFor(owner: string, role: "api-key" | "server-key", value: string) {
|
|
279
283
|
const suffix = `-${role}-${createHash("sha256").update(value).digest("hex").slice(0, 8)}`;
|
|
280
|
-
return `${owner.slice(0, NAME_MAX_LENGTH - suffix.length)}${suffix}`;
|
|
284
|
+
return `${owner.slice(0, NAME_MAX_LENGTH - suffix.length).replace(/-+$/, "")}${suffix}`;
|
|
281
285
|
}
|
|
282
286
|
|
|
283
287
|
async function ensureSecret(profile: Profile, name: string, value: string) {
|
|
284
288
|
const found = await findByName(profile, "secrets", name, namedSchema);
|
|
285
289
|
if (!found) {
|
|
286
|
-
await callOrFail(profile, () =>
|
|
290
|
+
await callOrFail(profile, () =>
|
|
291
|
+
managementClient(profile).secrets.set({ name, displayName: {}, value }),
|
|
292
|
+
);
|
|
287
293
|
}
|
|
288
294
|
}
|
|
289
295
|
|
|
@@ -333,6 +339,7 @@ async function ensureProvider(profile: Profile, provider: Provider) {
|
|
|
333
339
|
await callOrFail(profile, () =>
|
|
334
340
|
managementClient(profile).modelProviders.create({
|
|
335
341
|
name: provider.name,
|
|
342
|
+
displayName: {},
|
|
336
343
|
baseUrl: provider.baseUrl,
|
|
337
344
|
secretName,
|
|
338
345
|
models,
|
|
@@ -349,6 +356,7 @@ async function ensureProvider(profile: Profile, provider: Provider) {
|
|
|
349
356
|
await callOrFail(profile, () =>
|
|
350
357
|
managementClient(profile).modelProviders.update({
|
|
351
358
|
id: found.id,
|
|
359
|
+
displayName: found.displayName,
|
|
352
360
|
baseUrl: found.baseUrl,
|
|
353
361
|
secretName,
|
|
354
362
|
models,
|
|
@@ -395,6 +403,7 @@ async function ensureServer(
|
|
|
395
403
|
await callOrFail(profile, () =>
|
|
396
404
|
managementClient(profile).servers.create({
|
|
397
405
|
name,
|
|
406
|
+
displayName: {},
|
|
398
407
|
description: "",
|
|
399
408
|
url,
|
|
400
409
|
keySecretName,
|
|
@@ -417,7 +426,7 @@ async function ensureServer(
|
|
|
417
426
|
await callOrFail(profile, () =>
|
|
418
427
|
managementClient(profile).servers.update({
|
|
419
428
|
id: server.id,
|
|
420
|
-
|
|
429
|
+
displayName: server.displayName,
|
|
421
430
|
description: server.description ?? "",
|
|
422
431
|
url,
|
|
423
432
|
keySecretName,
|
|
@@ -1030,7 +1030,7 @@ class Parser {
|
|
|
1030
1030
|
}
|
|
1031
1031
|
|
|
1032
1032
|
/** `a + b * (c - 1)`: `*` and `/` bind before `+` and `-`; a date plus or minus an INTERVAL folds to the date it names. */
|
|
1033
|
-
//
|
|
1033
|
+
// oxlint-disable-next-line m6d/no-explicit-return-type -- mutually recursive with factor and unit
|
|
1034
1034
|
private term(message: string): Term {
|
|
1035
1035
|
let left = this.factor(message);
|
|
1036
1036
|
while (this.peekPunct("+") || this.peekPunct("-")) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
import { isRecord } from "../json/index";
|
|
4
|
+
import { TIMEZONE_HEADER } from "../wire/index";
|
|
4
5
|
import { managementProcedureKinds } from "./procedures.generated";
|
|
5
6
|
import type { ManagementProcedures } from "./procedures.generated";
|
|
6
7
|
|
|
@@ -75,10 +76,19 @@ export class ManagementClientError extends Error {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Where management calls go: a console's origin and a management key
|
|
81
|
+
* (`cortex_mgmt_…`); a test hands in its own `fetch`. `locale` (`ar`) and
|
|
82
|
+
* `timeZone` (an IANA zone, `Asia/Dubai`) are the caller's, sent as
|
|
83
|
+
* `Accept-Language` and `X-Cortex-Timezone`: Cortex words its refusals in
|
|
84
|
+
* the one and reads a day range in the other, else English and UTC days. A
|
|
85
|
+
* host acting for many users builds a client per request.
|
|
86
|
+
*/
|
|
79
87
|
export type ManagementTarget = {
|
|
80
88
|
url: string;
|
|
81
89
|
key: string;
|
|
90
|
+
locale?: string;
|
|
91
|
+
timeZone?: string;
|
|
82
92
|
fetch?: (url: string, init: RequestInit) => Promise<Response>;
|
|
83
93
|
};
|
|
84
94
|
|
|
@@ -100,6 +110,8 @@ export async function callManagement(
|
|
|
100
110
|
headers: {
|
|
101
111
|
Authorization: `Bearer ${target.key}`,
|
|
102
112
|
...(method === "post" ? { "Content-Type": "application/json" } : {}),
|
|
113
|
+
...(target.locale ? { "Accept-Language": target.locale } : {}),
|
|
114
|
+
...(target.timeZone ? { [TIMEZONE_HEADER]: target.timeZone } : {}),
|
|
103
115
|
},
|
|
104
116
|
body: method === "post" && input !== undefined ? JSON.stringify(input) : undefined,
|
|
105
117
|
signal,
|