@m6d/cortex-cli 0.20.0 → 0.22.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 CHANGED
@@ -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. A server name stops at Cortex's 120 characters, so a longer project name is
66
- registered cut to that length.
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 --slug support --name Support --serverId <uuid>
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.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Scaffold Cortex projects and manage a Cortex deployment",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 { AGENT_SLUG, manifestIdOf, scaffoldFiles } from "@/scaffold/files";
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 ${AGENT_SLUG} agent on the first bun run dev`)
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 ${AGENT_SLUG} agent when you first run bun run dev?`,
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 ${AGENT_SLUG}, then prints where to chat`),
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"),
@@ -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, NAME_MAX_LENGTH, SETTING_BOUNDS } from "@cortex/contracts/manifest";
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: z.string().min(1).max(NAME_MAX_LENGTH),
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: z.string().min(1).max(NAME_MAX_LENGTH),
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 so the whole is a
274
- * name Cortex holds. A name then holds one value only, so a secret that
275
- * exists is the value .env holds, and a changed key gets a secret of its own
276
- * while every row presenting the old one keeps it.
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, () => managementClient(profile).secrets.set({ name, value }));
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
- name,
429
+ displayName: server.displayName,
421
430
  description: server.description ?? "",
422
431
  url,
423
432
  keySecretName,
@@ -60,6 +60,27 @@ export type LocationResult = LatLng & { accuracy?: number };
60
60
 
61
61
  export type RequestLocationOutcome = ClientToolOutcome<LocationResult>;
62
62
 
63
+ /**
64
+ * Asks the end user to connect their own account to an MCP server that signs
65
+ * in each end user. Its call is stamped with where the sign-in starts
66
+ * (`ConnectAccountBinding` in the wire contract).
67
+ */
68
+ export const CONNECT_ACCOUNT_TOOL = "connectAccount";
69
+
70
+ /** The model's arguments: the MCP server to connect, by name. */
71
+ export type ConnectAccountInput = { server: string };
72
+
73
+ /**
74
+ * `completed` once the account is connected with the access it needs,
75
+ * `cancelled` when the end user said Not now or cancelled the sign-in,
76
+ * `failed` when the sign-in came back without that access, or ran out
77
+ * first.
78
+ */
79
+ export type ConnectAccountOutcome = ClientToolOutcome<never>;
80
+
81
+ /** How long a sign-in Cortex starts stays open, and so how long the widget waits for its connection. */
82
+ export const SIGN_IN_TTL_MS = 10 * 60 * 1000;
83
+
63
84
  /**
64
85
  * Parse a `message` event into a handshake, or null when the origin doesn't
65
86
  * match the tool's `embedOrigin` or the payload isn't a well-formed handshake.
@@ -10,6 +10,7 @@ export {
10
10
  canCarrySecret,
11
11
  egressAllowedHosts,
12
12
  isEgressAllowedHost,
13
+ isLoopback,
13
14
  isPrivateAddress,
14
15
  normalizeHost,
15
16
  } from "./url";
@@ -79,7 +79,7 @@ export function isPrivateAddress(address: string) {
79
79
  }
80
80
 
81
81
  /** Loopback by its normalized spelling: `[::1]` arrives from a URL without its brackets. */
82
- function isLoopback(hostname: string) {
82
+ export function isLoopback(hostname: string) {
83
83
  return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
84
84
  }
85
85