@m6d/cortex-cli 0.12.1 → 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.
Files changed (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /package/src/{cc → management}/output.ts +0 -0
@@ -0,0 +1,112 @@
1
+ import { z } from "zod";
2
+
3
+ /*
4
+ * Triggers: named entry points on an agent. A firing is one invocation with a
5
+ * payload and starts at most one run. The end user's message is the built-in
6
+ * `message` trigger; every other trigger is fired by a caller over
7
+ * `POST /agents/:agentSlug/triggers/:name` with the trigger's secret as its
8
+ * bearer. These are the shapes Cortex answers that caller with.
9
+ */
10
+
11
+ export const MESSAGE_TRIGGER_NAME = "message";
12
+
13
+ /** The tool a trigger run with an output schema answers through; no other tool may take its name. */
14
+ export const TRIGGER_ANSWER_TOOL_NAME = "submitAnswer";
15
+
16
+ export const TRIGGER_OUTCOMES = [
17
+ "started",
18
+ /** The run parked on a tool that needs approval; `POST .../firings/:id/answer` resumes it. */
19
+ "awaiting_approval",
20
+ "completed",
21
+ "failed",
22
+ "rejected",
23
+ "dropped",
24
+ ] as const;
25
+ export type TriggerOutcome = (typeof TRIGGER_OUTCOMES)[number];
26
+
27
+ /**
28
+ * Why a firing was dropped: the thread was busy with another run, a call on
29
+ * it waits for a decision (answered 409: decide that firing first), the
30
+ * daily budget was spent, or the agent's request bucket was.
31
+ */
32
+ const TRIGGER_DROP_REASONS = ["busy", "approval_pending", "budget", "rate_limited"] as const;
33
+ export type TriggerDropReason = (typeof TRIGGER_DROP_REASONS)[number];
34
+
35
+ /** One server tool call a parked run waits on; `POST .../firings/:id/answer` decides it. */
36
+ export const pendingApprovalSchema = z.object({
37
+ toolCallId: z.string(),
38
+ toolName: z.string(),
39
+ input: z.unknown(),
40
+ });
41
+
42
+ export const triggerFiringResponseSchema = z.discriminatedUnion("outcome", [
43
+ z.object({
44
+ firingId: z.uuid(),
45
+ outcome: z.literal("started"),
46
+ threadId: z.uuid(),
47
+ runId: z.string(),
48
+ }),
49
+ /* The shapes a caller that waited (`?wait=`) gets instead of `started`: the run ended, or it parked for a decision. */
50
+ z.object({
51
+ firingId: z.uuid(),
52
+ outcome: z.literal("awaiting_approval"),
53
+ threadId: z.uuid(),
54
+ runId: z.string(),
55
+ approvals: z.array(pendingApprovalSchema),
56
+ }),
57
+ z.object({
58
+ firingId: z.uuid(),
59
+ outcome: z.literal("completed"),
60
+ threadId: z.uuid(),
61
+ runId: z.string(),
62
+ /** The validated answer when the trigger declares an output schema, else the run's last text (null when it wrote none), cut past 1 MiB with a trailing `…[truncated]`. */
63
+ output: z.unknown(),
64
+ }),
65
+ z.object({
66
+ firingId: z.uuid(),
67
+ outcome: z.literal("failed"),
68
+ threadId: z.uuid(),
69
+ runId: z.string(),
70
+ reason: z.string().nullable(),
71
+ }),
72
+ z.object({
73
+ firingId: z.uuid(),
74
+ outcome: z.literal("dropped"),
75
+ reason: z.enum(TRIGGER_DROP_REASONS),
76
+ threadId: z.uuid().nullable(),
77
+ }),
78
+ z.object({
79
+ firingId: z.uuid(),
80
+ outcome: z.literal("rejected"),
81
+ issues: z.array(z.object({ path: z.string(), message: z.string() })),
82
+ }),
83
+ ]);
84
+ export type TriggerFiringResponse = z.infer<typeof triggerFiringResponseSchema>;
85
+
86
+ /**
87
+ * `POST /agents/:agentSlug/triggers/:name/firings/:id/answer`: the decision on one parked call.
88
+ * A call id is as long as the model provider made it (some carry a signature), so only the
89
+ * request's body limit bounds it.
90
+ */
91
+ export const triggerFiringAnswerSchema = z.object({
92
+ toolCallId: z.string().min(1),
93
+ approved: z.boolean(),
94
+ });
95
+ export type TriggerFiringAnswer = z.infer<typeof triggerFiringAnswerSchema>;
96
+
97
+ /** One row of `GET /agents/:agentSlug/triggers/:name/firings`, and the answer of `.../firings/:id`. */
98
+ export const triggerFiringRecordSchema = z.object({
99
+ id: z.uuid(),
100
+ triggerName: z.string(),
101
+ threadId: z.uuid().nullable(),
102
+ runId: z.string().nullable(),
103
+ outcome: z.enum(TRIGGER_OUTCOMES),
104
+ reason: z.string().nullable(),
105
+ /** What a completed run answered with, as a fire's `completed` answer carries it; null otherwise. */
106
+ output: z.unknown(),
107
+ /** The calls an `awaiting_approval` firing waits on; filled by the single-firing read, empty in the list and for every other outcome. */
108
+ approvals: z.array(pendingApprovalSchema),
109
+ createdAt: z.iso.datetime(),
110
+ finishedAt: z.iso.datetime().nullable(),
111
+ });
112
+ export type TriggerFiringRecord = z.infer<typeof triggerFiringRecordSchema>;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The wire contract — every shape that crosses the HTTP/WebSocket boundary
3
- * between `@m6d/cortex-server` and the client SDKs (`@m6d/cortex-angular`,
3
+ * between Cortex and the client SDKs (`@m6d/cortex-angular`,
4
4
  * `@m6d/cortex-react`).
5
5
  *
6
6
  * Nothing here has a runtime dependency, and nothing here may grow one: this
@@ -40,6 +40,9 @@ export const ATTACHMENT_MIME_TYPES = [
40
40
 
41
41
  export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
42
42
 
43
+ /** The longest name an attachment takes, uploaded or produced by a tool. */
44
+ export const ATTACHMENT_FILENAME_MAX_LENGTH = 255;
45
+
43
46
  export const MAX_ATTACHMENTS_PER_MESSAGE = 5;
44
47
 
45
48
  export const ATTACHMENT_STATUSES = ["pending", "described", "failed", "skipped"] as const;
@@ -49,9 +52,11 @@ export type AttachmentStatus = (typeof ATTACHMENT_STATUSES)[number];
49
52
  export const ATTACHMENT_ORIGINS = ["upload", "tool"] as const;
50
53
  export type AttachmentOrigin = (typeof ATTACHMENT_ORIGINS)[number];
51
54
 
52
- export function checkAttachmentPolicy(contentType: string, sizeBytes: number) {
53
- if (!ATTACHMENT_MIME_TYPES.some((type) => type === contentType)) return "type";
54
- if (sizeBytes > ATTACHMENT_MAX_BYTES) return "size";
55
+ /** Whether an upload fits the attachment policy, judged by the widget before it sends one and by Cortex as it takes one. */
56
+ export function checkAttachmentPolicy(file: { type: string; size: number; name: string }) {
57
+ if (!ATTACHMENT_MIME_TYPES.some((type) => type === file.type)) return "type";
58
+ if (file.size > ATTACHMENT_MAX_BYTES) return "size";
59
+ if (file.name.length > ATTACHMENT_FILENAME_MAX_LENGTH) return "name";
55
60
  return "ok";
56
61
  }
57
62
 
@@ -75,6 +80,15 @@ export type ThreadSummary = {
75
80
  trigger?: { name: string };
76
81
  };
77
82
 
83
+ /**
84
+ * Body of `POST /threads`. Only a console operator's request is read: its
85
+ * test thread keeps `variables` as the session the agent's server would
86
+ * otherwise fill per end user.
87
+ */
88
+ export type CreateThreadRequest = {
89
+ variables?: Record<string, string | number | boolean>;
90
+ };
91
+
78
92
  /**
79
93
  * Response of `POST /chat/:threadId/abort`. `aborted: false` means there was no
80
94
  * live run to abort — the turn had already committed — so the caller must not
@@ -100,9 +114,8 @@ export type TokenUsage = {
100
114
  };
101
115
 
102
116
  /**
103
- * How to launch one client tool: stamped by the server, per tool name, on
104
- * the metadata of an assistant message that may carry its pending call. Kept in
105
- * the message so the binding survives server restarts while a run is parked.
117
+ * How Cortex opens an embedded client tool's page for one call. Stored on the
118
+ * call, so the binding survives a restart while a run is parked.
106
119
  */
107
120
  export type ClientToolBinding = {
108
121
  toolId: string;
@@ -113,8 +126,8 @@ export type ClientToolBinding = {
113
126
  };
114
127
 
115
128
  /**
116
- * What the server offers this agent's clients, fetched once at widget startup.
117
- * The widget hides UI for anything absent; enforcement stays server-side.
129
+ * What Cortex offers this agent's clients, fetched once at widget startup.
130
+ * The widget hides UI for anything absent; Cortex enforces each.
118
131
  */
119
132
  export type AgentCapabilities = {
120
133
  attachments: boolean;
@@ -127,14 +140,13 @@ export type AgentCapabilities = {
127
140
  };
128
141
 
129
142
  export type MessageMetadata = {
130
- /** ISO time the server stored the message; stamped from the row when a transcript is served. */
143
+ /** ISO time Cortex stored the message; stamped from the row when a transcript is served. */
131
144
  createdAt?: string;
132
145
  modelId?: string;
133
146
  providerMetadata?: unknown;
134
147
  isAborted?: boolean;
135
148
  tokenUsage?: TokenUsage;
136
149
  attachments?: AttachmentSummary[];
137
- clientTools?: Record<string, ClientToolBinding>;
138
150
  /** Set on `trigger` messages: the firing that wrote them. */
139
151
  trigger?: TriggerFiringSummary;
140
152
  };
@@ -145,7 +157,7 @@ export type MessageMetadata = {
145
157
  */
146
158
  export type Suggestion = { title: string; prompt: string };
147
159
 
148
- /** The runtime contract's `suggestionsSchema` and `greetingSchema` are built from these. */
160
+ /** The manifest's suggestion and greeting schemas are built from these. */
149
161
  export type Suggestions = Record<Locale, Suggestion[]>;
150
162
  export type Greeting = Record<Locale, string>;
151
163
 
@@ -160,14 +172,32 @@ export type LocalizedText = Record<Locale, string>;
160
172
  export const STATEMENT_TOOLS = ["executeCode", "queryDataset", "describeDataset"] as const;
161
173
 
162
174
  /**
163
- * What cortex stamps on a tool-call part's `metadata.cortex`: the action the
164
- * approval card names when the call needs the user's approval, in the
165
- * author's words rather than the function's name.
175
+ * The input Cortex adds to every tool that waits for the end user's approval:
176
+ * the model writes what that one call will do, and the approval card shows it
177
+ * as its title. Cortex removes it before the call runs.
178
+ */
179
+ export const APPROVAL_TITLE_FIELD = "approvalTitle";
180
+
181
+ /**
182
+ * What Cortex stamps on a tool call's `metadata.cortex` from its first chunk,
183
+ * so the live part and the stored one agree.
166
184
  */
167
185
  export type ToolCallMetadata = {
168
- approval?: LocalizedText;
186
+ /**
187
+ * Set on every call the client answers: a client tool's, the consumer's own, or a built-in
188
+ * input's. Cortex runs every other call, so only these take an output from a request.
189
+ */
190
+ answeredBy?: "client";
191
+ /** Set beside `answeredBy` on a call of a client tool with an embedded page: the widget mounts the embed for it. */
192
+ embed?: ClientToolBinding;
169
193
  };
170
194
 
195
+ /** The `metadata.cortex` a tool call carries, when Cortex stamped one. */
196
+ export function toolCallMetadataOf(part: { metadata?: unknown }) {
197
+ const metadata = part.metadata as { cortex?: ToolCallMetadata | null } | null | undefined;
198
+ return metadata?.cortex ?? undefined;
199
+ }
200
+
171
201
  export type TriggerFiringSummary = {
172
202
  name: string;
173
203
  firingId: string;
@@ -177,7 +207,7 @@ export type TriggerFiringSummary = {
177
207
 
178
208
  /**
179
209
  * A stored message as it is persisted and served: the SDK's parts plus the
180
- * server-derived envelope the UIs render, carried in the SDK message's own
210
+ * envelope Cortex derives for the UIs, carried in the SDK message's own
181
211
  * `metadata` bag so it travels with the message over the stream and the
182
212
  * transcript alike.
183
213
  *
@@ -203,7 +233,7 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
203
233
 
204
234
  /**
205
235
  * Key under the AG-UI request's `forwardedProps` carrying the answers to
206
- * parked client tool calls on a continuation request. The server owns the
236
+ * parked client tool calls on a continuation request. Cortex owns the
207
237
  * transcript (a continuation sends `messages: []`), so the answered assistant
208
238
  * message itself never crosses the wire — the SDK's park-boundary snapshot
209
239
  * does not preserve its id, and round-tripping it would duplicate the stored
@@ -212,7 +242,7 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
212
242
  export const CLIENT_TOOL_ANSWERS_KEY = "clientToolAnswers";
213
243
 
214
244
  /**
215
- * One answered call, applied to the stored transcript server-side: a client
245
+ * One answered call, which Cortex applies to the stored transcript: a client
216
246
  * tool's output (`complete` / `error`), or the user's decision on a server
217
247
  * tool that asked for approval (`approved` / `denied`, output null).
218
248
  */
@@ -232,19 +262,15 @@ export function approvalIdOf(part: { id: string; approval?: { id: string } }) {
232
262
  }
233
263
 
234
264
  /**
235
- * Response of `POST /chat/:threadId/tools/:toolCallId/initiate`. `embed:
236
- * false` means the pending call is not a CC client tool — the widget
237
- * falls back to its default rendering for the call. The payload is for the
238
- * widget only; it never reaches the LLM.
265
+ * Response of `POST /chat/:threadId/tools/:toolCallId/initiate` for a pending
266
+ * call stamped with an embed. The payload is for the widget only; it never
267
+ * reaches the LLM.
239
268
  */
240
- export type ClientToolInitiateResult =
241
- | { embed: false }
242
- | {
243
- embed: true;
244
- embedUrl: string;
245
- surface: ToolSurface;
246
- embedOrigin: string;
247
- };
269
+ export type ClientToolInitiateResult = {
270
+ embedUrl: string;
271
+ surface: ToolSurface;
272
+ embedOrigin: string;
273
+ };
248
274
 
249
275
  export type ThreadCreatedEvent = {
250
276
  type: "thread:created";
@@ -261,14 +287,22 @@ export type ThreadTitleUpdatedEvent = {
261
287
  payload: { thread: ThreadSummary };
262
288
  };
263
289
 
290
+ /** Run `runId` took the thread; the `thread:run-finished` naming the same run ends it. */
264
291
  export type ThreadRunStartedEvent = {
265
292
  type: "thread:run-started";
266
- payload: { thread: ThreadSummary };
293
+ payload: { thread: ThreadSummary; runId: string };
267
294
  };
268
295
 
296
+ /**
297
+ * Run `runId` ended, committed or not, and left the thread free, so it takes
298
+ * a new message now; whatever turn it committed is in the transcript. Sent
299
+ * only for a run announced as started, and never by one whose thread another
300
+ * run had already taken, so a finish naming a run other than the one a
301
+ * client saw start last is stale.
302
+ */
269
303
  export type ThreadRunFinishedEvent = {
270
304
  type: "thread:run-finished";
271
- payload: { thread: ThreadSummary };
305
+ payload: { thread: ThreadSummary; runId: string };
272
306
  };
273
307
 
274
308
  export type ThreadMessagesUpdatedEvent = {
@@ -10,7 +10,7 @@ export type OpenApiOperation = {
10
10
  operationId: string;
11
11
  summary?: string;
12
12
  tags?: string[];
13
- /** The Control Center's own semantics, so nothing here is inferred from names or prose. */
13
+ /** Cortex's own semantics, so nothing here is inferred from names or prose. */
14
14
  "x-destructive"?: true;
15
15
  "x-publish-procedure"?: string;
16
16
  "x-live-fields"?: string[];
@@ -29,7 +29,8 @@ export type JsonSchema = {
29
29
  properties?: Record<string, JsonSchema>;
30
30
  required?: string[];
31
31
  enum?: unknown[];
32
- items?: JsonSchema;
32
+ /** `false` on a tuple, whose positions `prefixItems` lists instead. */
33
+ items?: JsonSchema | boolean;
33
34
  anyOf?: JsonSchema[];
34
35
  default?: unknown;
35
36
  description?: string;
@@ -45,7 +46,7 @@ export type OpenApiDocument = {
45
46
  type Cached = { etag: string; document: OpenApiDocument };
46
47
 
47
48
  /**
48
- * One cache entry per Control Center base URL (scheme, host, port, path),
49
+ * One cache entry per Cortex base URL (scheme, host, port, path),
49
50
  * so an http and an https profile, or two deployments under one host, never
50
51
  * read each other's document. The host keeps the file recognizable; a hash
51
52
  * of the whole URL keeps two URLs from ever sharing a name.
@@ -85,7 +86,7 @@ const SCHEMA_FIELDS = {
85
86
  properties: optional(recordOf(isJsonSchema)),
86
87
  required: optional(isStrings),
87
88
  enum: optional(Array.isArray),
88
- items: optional(isJsonSchema),
89
+ items: optional((v) => typeof v === "boolean" || isJsonSchema(v)),
89
90
  anyOf: optional(arrayOf(isJsonSchema)),
90
91
  description: optional(isString),
91
92
  format: optional(isString),
@@ -207,7 +208,7 @@ export function inputSchemaOf(operation: OpenApiOperation) {
207
208
  * string, one parameter per field, with a scalar written plainly and
208
209
  * anything else JSON-encoded, which is how the document declares it. A
209
210
  * mutation posts its input as the JSON body. A refusal throws
210
- * `ManagementError` with the error body's code and message.
211
+ * `ManagementError` with the error body's code, message and detail.
211
212
  */
212
213
  export async function callManagement(
213
214
  profile: { url: string; key: string },
@@ -224,11 +225,22 @@ export async function callManagement(
224
225
  ...(method === "post" ? { "Content-Type": "application/json" } : {}),
225
226
  },
226
227
  body: method === "post" && input !== undefined ? JSON.stringify(input) : undefined,
228
+ }).catch((error: unknown) => {
229
+ throw new UnreachableError(error);
227
230
  });
228
231
  const body: unknown = await response.json().catch(() => null);
229
232
  if (!response.ok) {
230
- const { code, message } = (body ?? {}) as { code?: string; message?: string };
231
- throw new ManagementError(response.status, code ?? "error", message ?? response.statusText);
233
+ const { code, message, cause } = (body ?? {}) as {
234
+ code?: string;
235
+ message?: string;
236
+ cause?: unknown;
237
+ };
238
+ throw new ManagementError(
239
+ response.status,
240
+ code ?? "error",
241
+ message ?? response.statusText,
242
+ isRecord(cause) ? cause : undefined,
243
+ );
232
244
  }
233
245
  return body;
234
246
  }
@@ -251,11 +263,25 @@ function queryString(input: Record<string, unknown>) {
251
263
  return encoded === "" ? "" : `?${encoded}`;
252
264
  }
253
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
+
278
+ /** A refusal; `detail` is what the refusal names, when it names something (an entry, the row holding a name). */
254
279
  export class ManagementError extends Error {
255
280
  public constructor(
256
281
  public readonly status: number,
257
282
  public readonly code: string,
258
283
  message: string,
284
+ public readonly detail?: Record<string, unknown>,
259
285
  ) {
260
286
  super(message);
261
287
  this.name = "ManagementError";
@@ -44,7 +44,8 @@ function kindOf(property: JsonSchema, document: OpenApiDocument) {
44
44
  if (type === "number" || type === "integer") return "number" as const;
45
45
  if (type === "boolean") return "boolean" as const;
46
46
  if (type === "array") {
47
- const item = property.items ? resolve(property.items, document) : undefined;
47
+ const item =
48
+ typeof property.items === "object" ? resolve(property.items, document) : undefined;
48
49
  const scalar = item && ["string", "number", "integer"].includes(typeOf(item) ?? "");
49
50
  return scalar ? ("array" as const) : ("json" as const);
50
51
  }
@@ -7,9 +7,9 @@ import { isRecord } from "@cortex/contracts/json";
7
7
  type Profile = { url: string; key: string };
8
8
  type Store = { current: string; profiles: Record<string, Profile> };
9
9
 
10
- /** `~/.cortex/cc.json`; `CORTEX_HOME` relocates it (tests, CI). */
10
+ /** `~/.cortex/profiles.json`; `CORTEX_HOME` relocates it (tests, CI). */
11
11
  export function profilesPath() {
12
- return join(process.env["CORTEX_HOME"] ?? join(homedir(), ".cortex"), "cc.json");
12
+ return join(process.env["CORTEX_HOME"] ?? join(homedir(), ".cortex"), "profiles.json");
13
13
  }
14
14
 
15
15
  /** Thrown when the profile file exists but is not one this CLI wrote. */
@@ -87,14 +87,16 @@ export function removeProfile(name: string) {
87
87
  }
88
88
 
89
89
  /**
90
- * The profile a `cc` command runs against: `CORTEX_CC_URL` + `CORTEX_CC_KEY`
91
- * when both are set (CI, containers), else the named or current entry from
92
- * the file. `undefined` means nobody has logged in.
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
- const url = process.env["CORTEX_CC_URL"];
96
- const key = process.env["CORTEX_CC_KEY"];
97
- if (url && key) return { name: "env", url: url.replace(/\/+$/, ""), key };
97
+ const url = process.env["CORTEX_URL"];
98
+ const key = process.env["CORTEX_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);