@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.
- package/README.md +193 -128
- package/package.json +16 -5
- package/src/cli.ts +19 -12
- package/src/commands/apply.ts +186 -0
- package/src/commands/{cc.ts → management.ts} +173 -112
- package/src/commands/new.ts +83 -84
- package/src/commands/setup.ts +610 -0
- package/src/contracts/charts/chart-spec.ts +1 -1
- package/src/contracts/client-tools/index.ts +3 -3
- package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
- package/src/contracts/datasets/index.ts +3 -0
- package/src/contracts/egress-policy/index.ts +16 -0
- package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
- package/src/contracts/{egress → egress-policy}/url.ts +22 -9
- package/src/contracts/errors/index.ts +38 -0
- package/src/contracts/json/index.ts +5 -0
- package/src/contracts/manifest/index.ts +973 -0
- package/src/contracts/manifest-file/index.ts +16 -0
- package/src/contracts/markdown/index.ts +1 -1
- package/src/contracts/pattern/index.ts +276 -0
- package/src/contracts/server/index.ts +349 -0
- package/src/contracts/server/reserved-names.fixtures.json +18 -0
- package/src/contracts/triggers/index.ts +112 -0
- package/src/contracts/wire/index.ts +67 -33
- package/src/{cc → management}/client.ts +33 -7
- package/src/{cc → management}/flags.ts +2 -1
- package/src/{cc → management}/profiles.ts +10 -8
- package/src/scaffold/files.ts +448 -284
- package/src/scaffold/ports.ts +12 -24
- package/src/scaffold/templates/cspell.json.txt +41 -0
- package/src/scaffold/templates/datasets.ts.txt +52 -0
- package/src/scaffold/templates/dockerignore.txt +6 -0
- package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
- package/src/scaffold/templates/gitignore.txt +1 -1
- package/src/scaffold/templates/server.ts.txt +40 -0
- package/src/scaffold/templates/setup.ts.txt +44 -0
- package/src/ui/prompts.ts +24 -38
- package/src/ui/report.ts +5 -15
- package/tsconfig.json +2 -3
- package/src/commands/graph.ts +0 -228
- package/src/commands/swagger.ts +0 -247
- package/src/config/load.ts +0 -122
- package/src/config/validate.ts +0 -46
- package/src/contracts/credentials/index.ts +0 -183
- package/src/contracts/egress/index.ts +0 -120
- package/src/contracts/graph/clients/embed.ts +0 -62
- package/src/contracts/graph/clients/neo4j.ts +0 -85
- package/src/contracts/graph/helpers.ts +0 -55
- package/src/contracts/graph/index.ts +0 -30
- package/src/contracts/graph/schema.ts +0 -65
- package/src/contracts/graph/types.ts +0 -125
- package/src/contracts/rich-text/index.ts +0 -220
- package/src/contracts/runtime/api.ts +0 -345
- package/src/contracts/runtime/auth.ts +0 -41
- package/src/contracts/runtime/grammar.ts +0 -169
- package/src/contracts/runtime/index.ts +0 -15
- package/src/contracts/runtime/models.ts +0 -50
- package/src/contracts/runtime/signature.ts +0 -255
- package/src/contracts/runtime/sync.ts +0 -196
- package/src/contracts/runtime/triggers.ts +0 -267
- package/src/graph/expand-domains.ts +0 -286
- package/src/graph/generate-cypher.ts +0 -201
- package/src/graph/seed.ts +0 -199
- package/src/graph/validate.ts +0 -78
- package/src/scaffold/features.ts +0 -190
- package/src/scaffold/templates/domains-README.md.txt +0 -16
- package/src/scaffold/templates/index.ts.txt +0 -18
- package/src/swagger/extract-endpoints.ts +0 -269
- package/src/swagger/generated-block.ts +0 -74
- package/src/swagger/openapi-schema.ts +0 -168
- package/src/swagger/parse-response.ts +0 -88
- package/src/swagger/types.ts +0 -25
- /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
- /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
- /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
- /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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
if (
|
|
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
|
|
104
|
-
*
|
|
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
|
|
117
|
-
* The widget hides UI for anything absent;
|
|
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
|
|
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
|
|
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
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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,
|
|
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
|
|
236
|
-
*
|
|
237
|
-
*
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
231
|
-
|
|
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 =
|
|
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/
|
|
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"), "
|
|
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
|
|
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
|
-
const url = process.env["
|
|
96
|
-
const key = process.env["
|
|
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);
|