@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.
Files changed (74) hide show
  1. package/README.md +135 -130
  2. package/package.json +16 -5
  3. package/src/cli.ts +17 -12
  4. package/src/commands/apply.ts +169 -0
  5. package/src/commands/{cc.ts → management.ts} +150 -100
  6. package/src/commands/new.ts +50 -74
  7. package/src/contracts/charts/chart-spec.ts +1 -1
  8. package/src/contracts/client-tools/index.ts +3 -3
  9. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  10. package/src/contracts/datasets/index.ts +3 -0
  11. package/src/contracts/egress-policy/index.ts +16 -0
  12. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  13. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  14. package/src/contracts/errors/index.ts +38 -0
  15. package/src/contracts/json/index.ts +5 -0
  16. package/src/contracts/manifest/index.ts +932 -0
  17. package/src/contracts/manifest-file/index.ts +16 -0
  18. package/src/contracts/markdown/index.ts +1 -1
  19. package/src/contracts/pattern/index.ts +276 -0
  20. package/src/contracts/server/index.ts +247 -0
  21. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  22. package/src/contracts/triggers/index.ts +112 -0
  23. package/src/contracts/wire/index.ts +61 -32
  24. package/src/{cc → management}/client.ts +19 -7
  25. package/src/{cc → management}/flags.ts +2 -1
  26. package/src/{cc → management}/profiles.ts +5 -5
  27. package/src/scaffold/files.ts +362 -237
  28. package/src/scaffold/ports.ts +13 -24
  29. package/src/scaffold/templates/datasets.ts.txt +52 -0
  30. package/src/scaffold/templates/dockerignore.txt +7 -0
  31. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  32. package/src/scaffold/templates/gitignore.txt +2 -1
  33. package/src/scaffold/templates/server.ts.txt +40 -0
  34. package/src/scaffold/templates/setup.ts.txt +52 -0
  35. package/src/ui/prompts.ts +4 -31
  36. package/src/ui/report.ts +5 -15
  37. package/tsconfig.json +2 -3
  38. package/src/commands/graph.ts +0 -228
  39. package/src/commands/swagger.ts +0 -247
  40. package/src/config/load.ts +0 -122
  41. package/src/config/validate.ts +0 -46
  42. package/src/contracts/credentials/index.ts +0 -183
  43. package/src/contracts/egress/index.ts +0 -120
  44. package/src/contracts/graph/clients/embed.ts +0 -45
  45. package/src/contracts/graph/clients/neo4j.ts +0 -85
  46. package/src/contracts/graph/helpers.ts +0 -55
  47. package/src/contracts/graph/index.ts +0 -30
  48. package/src/contracts/graph/schema.ts +0 -65
  49. package/src/contracts/graph/types.ts +0 -125
  50. package/src/contracts/rich-text/index.ts +0 -220
  51. package/src/contracts/runtime/api.ts +0 -345
  52. package/src/contracts/runtime/auth.ts +0 -41
  53. package/src/contracts/runtime/grammar.ts +0 -169
  54. package/src/contracts/runtime/index.ts +0 -15
  55. package/src/contracts/runtime/models.ts +0 -50
  56. package/src/contracts/runtime/signature.ts +0 -255
  57. package/src/contracts/runtime/sync.ts +0 -196
  58. package/src/contracts/runtime/triggers.ts +0 -267
  59. package/src/graph/expand-domains.ts +0 -286
  60. package/src/graph/generate-cypher.ts +0 -201
  61. package/src/graph/seed.ts +0 -199
  62. package/src/graph/validate.ts +0 -78
  63. package/src/scaffold/features.ts +0 -190
  64. package/src/scaffold/templates/domains-README.md.txt +0 -16
  65. package/src/scaffold/templates/index.ts.txt +0 -18
  66. package/src/swagger/extract-endpoints.ts +0 -269
  67. package/src/swagger/generated-block.ts +0 -74
  68. package/src/swagger/openapi-schema.ts +0 -168
  69. package/src/swagger/parse-response.ts +0 -88
  70. package/src/swagger/types.ts +0 -25
  71. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  72. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  73. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  74. /package/src/{cc → management}/output.ts +0 -0
@@ -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,27 @@ 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
+ * What Cortex stamps on a tool call's `metadata.cortex` from its first chunk,
176
+ * so the live part and the stored one agree.
166
177
  */
167
178
  export type ToolCallMetadata = {
179
+ /** The action the approval card names, in the author's words rather than the function's name. */
168
180
  approval?: LocalizedText;
181
+ /**
182
+ * Set on every call the client answers: a client tool's, the consumer's own, or a built-in
183
+ * input's. Cortex runs every other call, so only these take an output from a request.
184
+ */
185
+ answeredBy?: "client";
186
+ /** Set beside `answeredBy` on a call of a client tool with an embedded page: the widget mounts the embed for it. */
187
+ embed?: ClientToolBinding;
169
188
  };
170
189
 
190
+ /** The `metadata.cortex` a tool call carries, when Cortex stamped one. */
191
+ export function toolCallMetadataOf(part: { metadata?: unknown }) {
192
+ const metadata = part.metadata as { cortex?: ToolCallMetadata | null } | null | undefined;
193
+ return metadata?.cortex ?? undefined;
194
+ }
195
+
171
196
  export type TriggerFiringSummary = {
172
197
  name: string;
173
198
  firingId: string;
@@ -177,7 +202,7 @@ export type TriggerFiringSummary = {
177
202
 
178
203
  /**
179
204
  * 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
205
+ * envelope Cortex derives for the UIs, carried in the SDK message's own
181
206
  * `metadata` bag so it travels with the message over the stream and the
182
207
  * transcript alike.
183
208
  *
@@ -203,7 +228,7 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
203
228
 
204
229
  /**
205
230
  * 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
231
+ * parked client tool calls on a continuation request. Cortex owns the
207
232
  * transcript (a continuation sends `messages: []`), so the answered assistant
208
233
  * message itself never crosses the wire — the SDK's park-boundary snapshot
209
234
  * does not preserve its id, and round-tripping it would duplicate the stored
@@ -212,7 +237,7 @@ export function toUiMessage<TPart>(message: CortexMessage<TPart>) {
212
237
  export const CLIENT_TOOL_ANSWERS_KEY = "clientToolAnswers";
213
238
 
214
239
  /**
215
- * One answered call, applied to the stored transcript server-side: a client
240
+ * One answered call, which Cortex applies to the stored transcript: a client
216
241
  * tool's output (`complete` / `error`), or the user's decision on a server
217
242
  * tool that asked for approval (`approved` / `denied`, output null).
218
243
  */
@@ -232,19 +257,15 @@ export function approvalIdOf(part: { id: string; approval?: { id: string } }) {
232
257
  }
233
258
 
234
259
  /**
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.
260
+ * Response of `POST /chat/:threadId/tools/:toolCallId/initiate` for a pending
261
+ * call stamped with an embed. The payload is for the widget only; it never
262
+ * reaches the LLM.
239
263
  */
240
- export type ClientToolInitiateResult =
241
- | { embed: false }
242
- | {
243
- embed: true;
244
- embedUrl: string;
245
- surface: ToolSurface;
246
- embedOrigin: string;
247
- };
264
+ export type ClientToolInitiateResult = {
265
+ embedUrl: string;
266
+ surface: ToolSurface;
267
+ embedOrigin: string;
268
+ };
248
269
 
249
270
  export type ThreadCreatedEvent = {
250
271
  type: "thread:created";
@@ -261,14 +282,22 @@ export type ThreadTitleUpdatedEvent = {
261
282
  payload: { thread: ThreadSummary };
262
283
  };
263
284
 
285
+ /** Run `runId` took the thread; the `thread:run-finished` naming the same run ends it. */
264
286
  export type ThreadRunStartedEvent = {
265
287
  type: "thread:run-started";
266
- payload: { thread: ThreadSummary };
288
+ payload: { thread: ThreadSummary; runId: string };
267
289
  };
268
290
 
291
+ /**
292
+ * Run `runId` ended, committed or not, and left the thread free, so it takes
293
+ * a new message now; whatever turn it committed is in the transcript. Sent
294
+ * only for a run announced as started, and never by one whose thread another
295
+ * run had already taken, so a finish naming a run other than the one a
296
+ * client saw start last is stale.
297
+ */
269
298
  export type ThreadRunFinishedEvent = {
270
299
  type: "thread:run-finished";
271
- payload: { thread: ThreadSummary };
300
+ payload: { thread: ThreadSummary; runId: string };
272
301
  };
273
302
 
274
303
  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 },
@@ -227,8 +228,17 @@ export async function callManagement(
227
228
  });
228
229
  const body: unknown = await response.json().catch(() => null);
229
230
  if (!response.ok) {
230
- const { code, message } = (body ?? {}) as { code?: string; message?: string };
231
- throw new ManagementError(response.status, code ?? "error", message ?? response.statusText);
231
+ const { code, message, cause } = (body ?? {}) as {
232
+ code?: string;
233
+ message?: string;
234
+ cause?: unknown;
235
+ };
236
+ throw new ManagementError(
237
+ response.status,
238
+ code ?? "error",
239
+ message ?? response.statusText,
240
+ isRecord(cause) ? cause : undefined,
241
+ );
232
242
  }
233
243
  return body;
234
244
  }
@@ -251,11 +261,13 @@ function queryString(input: Record<string, unknown>) {
251
261
  return encoded === "" ? "" : `?${encoded}`;
252
262
  }
253
263
 
264
+ /** A refusal; `detail` is what the refusal names, when it names something (an entry, the row holding a name). */
254
265
  export class ManagementError extends Error {
255
266
  public constructor(
256
267
  public readonly status: number,
257
268
  public readonly code: string,
258
269
  message: string,
270
+ public readonly detail?: Record<string, unknown>,
259
271
  ) {
260
272
  super(message);
261
273
  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,13 +87,13 @@ 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`
90
+ * The profile a management command runs against: `CORTEX_URL` + `CORTEX_KEY`
91
91
  * when both are set (CI, containers), else the named or current entry from
92
92
  * the file. `undefined` means nobody has logged in.
93
93
  */
94
94
  export function resolveProfile(name?: string) {
95
- const url = process.env["CORTEX_CC_URL"];
96
- const key = process.env["CORTEX_CC_KEY"];
95
+ const url = process.env["CORTEX_URL"];
96
+ const key = process.env["CORTEX_KEY"];
97
97
  if (url && key) return { name: "env", url: url.replace(/\/+$/, ""), key };
98
98
  const store = readStore();
99
99
  const chosen = name ?? store.current;