@m6d/cortex-cli 1.0.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.
@@ -0,0 +1,208 @@
1
+ import { z } from "zod";
2
+
3
+ /*
4
+ * The HTTP contract of the cortex-cc runtime API: the console (`apps/cortex-cc`)
5
+ * validates requests against these schemas and `@m6d/cortex-server` validates the
6
+ * responses it gets back. Request types use `z.input` because the callers build
7
+ * payloads the runtime's parser then fills defaults into.
8
+ */
9
+
10
+ /** Runtime contract §1.1: the `:id` path param, validated on both sides of the wire. */
11
+ export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
12
+
13
+ /** Runtime contract §5: tool names become `tools.<name>()` sandbox bindings. */
14
+ export const TOOL_NAME_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
15
+
16
+ export const PROMPT_VARIABLES = ["userName", "channel", "locale"] as const;
17
+
18
+ export const RUNTIME_ERROR_KINDS = [
19
+ "timeout",
20
+ "upstream_4xx",
21
+ "upstream_5xx",
22
+ "schema_mismatch",
23
+ "rate_limited",
24
+ "not_found",
25
+ "unauthorized",
26
+ "invalid_request",
27
+ "egress_blocked",
28
+ "internal",
29
+ ] as const;
30
+
31
+ /** The ways an execute output may disagree with the tool's declared schema. */
32
+ export const JSON_SCHEMA_ISSUE_KINDS = ["missing", "unexpected", "type_mismatch"] as const;
33
+
34
+ export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
35
+ export const localeSchema = z.enum(["en", "ar"]);
36
+ export const catalogVersionSchema = z.string().regex(/^[0-9a-f]{16}$/);
37
+
38
+ export const sharedShapeSchema = z.object({
39
+ ref: z.string().regex(/^\$R\d+$/),
40
+ shape: z.string(),
41
+ });
42
+
43
+ export const knowledgeChunkSchema = z.object({
44
+ chunkId: z.uuid(),
45
+ text: z.string(),
46
+ score: z.number(),
47
+ citation: z.object({
48
+ documentId: z.uuid(),
49
+ documentTitle: z.string(),
50
+ page: z.number().int().positive().nullable(),
51
+ section: z.string().nullable(),
52
+ uri: z.url().nullable(),
53
+ service: z.string().nullable(),
54
+ }),
55
+ });
56
+
57
+ export const toolSignatureSchema = z.object({
58
+ toolId: z.uuid(),
59
+ name: z.string().regex(TOOL_NAME_PATTERN),
60
+ version: z.number().int().positive(),
61
+ readOnly: z.boolean(),
62
+ tags: z.array(z.string()),
63
+ signature: z.string(),
64
+ score: z.number().nullable(),
65
+ pinned: z.boolean(),
66
+ });
67
+
68
+ export const serviceCardSchema = z.object({
69
+ serviceId: z.uuid(),
70
+ key: z
71
+ .string()
72
+ .regex(/^[a-z0-9]+(-[a-z0-9]+)*$/)
73
+ .max(64),
74
+ name: z.string(),
75
+ card: z.string(),
76
+ score: z.number(),
77
+ });
78
+
79
+ export const runtimeAgentConfigSchema = z.object({
80
+ agentId: agentSlugSchema,
81
+ systemPrompt: z.string(),
82
+ promptVariables: z.array(z.enum(PROMPT_VARIABLES)),
83
+ catalogBlurb: z.string(),
84
+ defaultLocale: localeSchema,
85
+ metaTools: z.object({
86
+ searchKnowledge: z.boolean(),
87
+ searchTools: z.boolean(),
88
+ searchServices: z.boolean(),
89
+ }),
90
+ limits: z.object({
91
+ resolveTokenBudget: z.number().int().positive(),
92
+ executeTimeoutMs: z.number().int().positive(),
93
+ maxResponseBytes: z.number().int().positive(),
94
+ }),
95
+ catalogVersion: catalogVersionSchema,
96
+ publishedAt: z.iso.datetime(),
97
+ });
98
+
99
+ export const resolveRequestSchema = z.object({
100
+ query: z.string().trim().min(1).max(4000),
101
+ locale: localeSchema.optional(),
102
+ hints: z
103
+ .object({
104
+ threadId: z.string().max(128).optional(),
105
+ recentToolNames: z.array(z.string()).max(20).default([]),
106
+ turnIndex: z.number().int().min(0).optional(),
107
+ })
108
+ .optional(),
109
+ tokenBudget: z.number().int().min(500).max(20000).optional(),
110
+ limits: z
111
+ .object({
112
+ knowledge: z.number().int().min(0).max(20).default(6),
113
+ tools: z.number().int().min(0).max(20).default(8),
114
+ services: z.number().int().min(0).max(20).default(5),
115
+ })
116
+ .optional(),
117
+ });
118
+
119
+ export const resolveResponseSchema = z.object({
120
+ resolveId: z.uuid(),
121
+ catalogVersion: catalogVersionSchema,
122
+ locale: localeSchema,
123
+ knowledge: z.array(knowledgeChunkSchema),
124
+ tools: z.array(toolSignatureSchema),
125
+ services: z.array(serviceCardSchema),
126
+ sharedShapes: z.array(sharedShapeSchema),
127
+ budget: z.object({
128
+ requested: z.number().int(),
129
+ used: z.number().int(),
130
+ truncated: z.boolean(),
131
+ }),
132
+ });
133
+
134
+ export const searchRequestSchema = z.object({
135
+ query: z.string().trim().min(1).max(4000),
136
+ limit: z.number().int().min(1).max(20).default(5),
137
+ locale: localeSchema.optional(),
138
+ threadId: z.string().max(128).optional(),
139
+ service: z.string().max(64).optional(),
140
+ });
141
+
142
+ export const searchToolsResponseSchema = z.object({
143
+ catalogVersion: catalogVersionSchema,
144
+ tools: z.array(toolSignatureSchema),
145
+ sharedShapes: z.array(sharedShapeSchema),
146
+ });
147
+
148
+ export const searchServicesResponseSchema = z.object({
149
+ catalogVersion: catalogVersionSchema,
150
+ services: z.array(serviceCardSchema),
151
+ tools: z.array(toolSignatureSchema),
152
+ sharedShapes: z.array(sharedShapeSchema),
153
+ });
154
+
155
+ export const searchKnowledgeResponseSchema = z.object({
156
+ catalogVersion: catalogVersionSchema,
157
+ knowledge: z.array(knowledgeChunkSchema),
158
+ });
159
+
160
+ export const executeRequestSchema = z.object({
161
+ input: z.record(z.string(), z.unknown()).default({}),
162
+ context: z
163
+ .object({
164
+ threadId: z.string().max(128).optional(),
165
+ userId: z.string().max(128).optional(),
166
+ locale: localeSchema.optional(),
167
+ })
168
+ .optional(),
169
+ });
170
+
171
+ export const executeResponseSchema = z.object({
172
+ toolId: z.uuid(),
173
+ name: z.string(),
174
+ version: z.number().int().positive(),
175
+ output: z.unknown(),
176
+ meta: z.object({
177
+ upstreamStatus: z.number().int().min(100).max(599),
178
+ durationMs: z.number().int().min(0),
179
+ replayed: z.boolean(),
180
+ truncated: z.boolean(),
181
+ truncatedPaths: z.array(z.string()),
182
+ drift: z.array(
183
+ z.object({
184
+ path: z.string(),
185
+ kind: z.enum(JSON_SCHEMA_ISSUE_KINDS),
186
+ }),
187
+ ),
188
+ }),
189
+ });
190
+
191
+ const runtimeErrorKindSchema = z.enum(RUNTIME_ERROR_KINDS);
192
+ export type RuntimeErrorKind = z.infer<typeof runtimeErrorKindSchema>;
193
+
194
+ export const runtimeErrorSchema = z.object({
195
+ error: z.object({
196
+ kind: runtimeErrorKindSchema,
197
+ detail: z.string().max(512),
198
+ retryable: z.boolean(),
199
+ retryAfterSeconds: z.number().int().positive().optional(),
200
+ upstreamStatus: z.number().int().min(100).max(599).optional(),
201
+ }),
202
+ });
203
+
204
+ export type RuntimeAgentConfig = z.infer<typeof runtimeAgentConfigSchema>;
205
+ export type ResolveRequest = z.input<typeof resolveRequestSchema>;
206
+ export type ResolveResponse = z.infer<typeof resolveResponseSchema>;
207
+ export type SearchRequest = z.input<typeof searchRequestSchema>;
208
+ export type ExecuteRequest = z.input<typeof executeRequestSchema>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * The wire contract — every shape that crosses the HTTP/WebSocket boundary
3
+ * between `@m6d/cortex-server` and the client SDKs (`@m6d/cortex-angular`,
4
+ * `@m6d/cortex-react`).
5
+ *
6
+ * Nothing here has a runtime dependency, and nothing here may grow one: this
7
+ * file is compiled into an Angular library, a React library and a Bun server
8
+ * alike.
9
+ */
10
+
11
+ export const ATTACHMENT_MIME_TYPES = [
12
+ "image/png",
13
+ "image/jpeg",
14
+ "image/webp",
15
+ "application/pdf",
16
+ ] as const;
17
+
18
+ export const ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
19
+
20
+ export const MAX_ATTACHMENTS_PER_MESSAGE = 5;
21
+
22
+ export const ATTACHMENT_STATUSES = ["pending", "described", "failed", "skipped"] as const;
23
+
24
+ export function checkAttachmentPolicy(contentType: string, sizeBytes: number) {
25
+ if (!ATTACHMENT_MIME_TYPES.some((type) => type === contentType)) return "type";
26
+ if (sizeBytes > ATTACHMENT_MAX_BYTES) return "size";
27
+ return "ok";
28
+ }
29
+
30
+ export type AttachmentStatus = (typeof ATTACHMENT_STATUSES)[number];
31
+
32
+ export type AttachmentSummary = {
33
+ id: string;
34
+ filename: string;
35
+ contentType: string;
36
+ sizeBytes: number;
37
+ status: AttachmentStatus;
38
+ description?: string;
39
+ };
40
+
41
+ export type ThreadSummary = {
42
+ id: string;
43
+ title?: string;
44
+ createdAt: string;
45
+ updatedAt: string;
46
+ isRunning: boolean;
47
+ };
48
+
49
+ /**
50
+ * Response of `POST /chat/:chatId/abort`. `aborted: false` means there was no
51
+ * live run to abort — the turn had already committed — so the caller must not
52
+ * present the turn as aborted and should read the settled state back instead.
53
+ */
54
+ export type AbortRunResult = {
55
+ aborted: boolean;
56
+ };
57
+
58
+ export type TokenUsage = {
59
+ input: {
60
+ noCache: number;
61
+ cacheRead: number;
62
+ cacheWrite: number;
63
+ total: number;
64
+ };
65
+ output: {
66
+ reasoning: number;
67
+ text: number;
68
+ total: number;
69
+ };
70
+ total: number;
71
+ };
72
+
73
+ export type MessageMetadata = {
74
+ modelId?: string;
75
+ providerMetadata?: unknown;
76
+ isAborted?: boolean;
77
+ tokenUsage?: TokenUsage;
78
+ attachments?: AttachmentSummary[];
79
+ };
80
+
81
+ /**
82
+ * A stored message as it is persisted and served. TanStack AI's `UIMessage`
83
+ * has no metadata field, so cortex owns the envelope: the SDK's parts plus the
84
+ * server-derived metadata the UI renders on settled messages.
85
+ *
86
+ * `TPart` stays generic so this file keeps its zero dependencies — both
87
+ * sides substitute the SDK's `MessagePart`.
88
+ */
89
+ export type CortexMessage<TPart = unknown> = {
90
+ id: string;
91
+ role: "system" | "user" | "assistant";
92
+ parts: TPart[];
93
+ metadata?: MessageMetadata;
94
+ };
95
+
96
+ export type ThreadCreatedEvent = {
97
+ type: "thread:created";
98
+ payload: { thread: ThreadSummary };
99
+ };
100
+
101
+ export type ThreadDeletedEvent = {
102
+ type: "thread:deleted";
103
+ payload: { threadId: string };
104
+ };
105
+
106
+ export type ThreadTitleUpdatedEvent = {
107
+ type: "thread:title-updated";
108
+ payload: { thread: ThreadSummary };
109
+ };
110
+
111
+ export type ThreadRunStartedEvent = {
112
+ type: "thread:run-started";
113
+ payload: { thread: ThreadSummary };
114
+ };
115
+
116
+ export type ThreadRunFinishedEvent = {
117
+ type: "thread:run-finished";
118
+ payload: { thread: ThreadSummary };
119
+ };
120
+
121
+ export type ThreadMessagesUpdatedEvent = {
122
+ type: "thread:messages-updated";
123
+ payload: { threadId: string; thread: ThreadSummary };
124
+ };
125
+
126
+ export type ToolProgressEvent = {
127
+ type: "thread:tool-progress";
128
+ payload: {
129
+ threadId: string;
130
+ toolCallId: string;
131
+ toolName: string;
132
+ status: "started" | "finished";
133
+ };
134
+ };
135
+
136
+ export type WsEvent =
137
+ | ThreadCreatedEvent
138
+ | ThreadDeletedEvent
139
+ | ThreadTitleUpdatedEvent
140
+ | ThreadRunStartedEvent
141
+ | ThreadRunFinishedEvent
142
+ | ThreadMessagesUpdatedEvent
143
+ | ToolProgressEvent;
@@ -0,0 +1,288 @@
1
+ import type {
2
+ ConceptDef,
3
+ DomainDef,
4
+ EndpointDef,
5
+ RuleDef,
6
+ ServiceDef,
7
+ } from "@cortex/contracts/graph";
8
+
9
+ type DomainCollections = {
10
+ concepts: ConceptDef[];
11
+ endpoints: EndpointDef[];
12
+ services: ServiceDef[];
13
+ rules: RuleDef[];
14
+ };
15
+
16
+ type OwnershipMaps = {
17
+ concepts: WeakMap<ConceptDef, string>;
18
+ endpoints: WeakMap<EndpointDef, string>;
19
+ services: WeakMap<ServiceDef, string>;
20
+ rules: WeakMap<RuleDef, string>;
21
+ };
22
+
23
+ export function expandDomains(domains: Record<string, DomainDef>) {
24
+ const ownership = createOwnershipMaps(domains);
25
+ const expanded: Record<string, DomainDef> = {};
26
+
27
+ for (const [domainKey, domain] of Object.entries(domains)) {
28
+ const collections = cloneCollections(domain);
29
+ const pendingConcepts = [...collections.concepts];
30
+ const pendingEndpoints = [...collections.endpoints];
31
+ const pendingServices = [...collections.services];
32
+
33
+ while (
34
+ pendingConcepts.length > 0 ||
35
+ pendingEndpoints.length > 0 ||
36
+ pendingServices.length > 0
37
+ ) {
38
+ const concept = pendingConcepts.pop();
39
+ if (concept) {
40
+ includeConcept({
41
+ domainKey,
42
+ concept,
43
+ ownership,
44
+ collections,
45
+ pendingConcepts,
46
+ pendingEndpoints,
47
+ });
48
+ }
49
+
50
+ const endpoint = pendingEndpoints.pop();
51
+ if (endpoint) {
52
+ includeEndpoint({
53
+ domainKey,
54
+ endpoint,
55
+ ownership,
56
+ collections,
57
+ pendingConcepts,
58
+ pendingEndpoints,
59
+ });
60
+ }
61
+
62
+ const service = pendingServices.pop();
63
+ if (service) {
64
+ includeService({
65
+ domainKey,
66
+ service,
67
+ ownership,
68
+ collections,
69
+ pendingConcepts,
70
+ });
71
+ }
72
+ }
73
+
74
+ expanded[domainKey] = {
75
+ ...domain,
76
+ concepts: collections.concepts,
77
+ endpoints: collections.endpoints,
78
+ services: collections.services,
79
+ rules: collections.rules,
80
+ };
81
+ }
82
+
83
+ return expanded;
84
+ }
85
+
86
+ function createOwnershipMaps(domains: Record<string, DomainDef>) {
87
+ const ownership: OwnershipMaps = {
88
+ concepts: new WeakMap<ConceptDef, string>(),
89
+ endpoints: new WeakMap<EndpointDef, string>(),
90
+ services: new WeakMap<ServiceDef, string>(),
91
+ rules: new WeakMap<RuleDef, string>(),
92
+ };
93
+
94
+ for (const [domainKey, domain] of Object.entries(domains)) {
95
+ for (const concept of domain.concepts ?? []) {
96
+ assignOwner(ownership.concepts, concept, domainKey);
97
+ }
98
+
99
+ for (const endpoint of domain.endpoints ?? []) {
100
+ assignOwner(ownership.endpoints, endpoint, domainKey);
101
+ }
102
+
103
+ for (const service of domain.services ?? []) {
104
+ assignOwner(ownership.services, service, domainKey);
105
+ }
106
+
107
+ for (const rule of domain.rules ?? []) {
108
+ assignOwner(ownership.rules, rule, domainKey);
109
+ }
110
+ }
111
+
112
+ return ownership;
113
+ }
114
+
115
+ function assignOwner<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
116
+ if (!owners.has(value)) {
117
+ owners.set(value, domainKey);
118
+ }
119
+ }
120
+
121
+ function cloneCollections(domain: DomainDef) {
122
+ return {
123
+ concepts: [...(domain.concepts ?? [])],
124
+ endpoints: [...(domain.endpoints ?? [])],
125
+ services: [...(domain.services ?? [])],
126
+ rules: [...(domain.rules ?? [])],
127
+ } satisfies DomainCollections;
128
+ }
129
+
130
+ function includeConcept(options: {
131
+ domainKey: string;
132
+ concept: ConceptDef;
133
+ ownership: OwnershipMaps;
134
+ collections: DomainCollections;
135
+ pendingConcepts: ConceptDef[];
136
+ pendingEndpoints: EndpointDef[];
137
+ }) {
138
+ const { domainKey, concept, ownership, collections, pendingConcepts, pendingEndpoints } =
139
+ options;
140
+
141
+ includeRule(domainKey, concept.governedBy ?? [], ownership, collections);
142
+
143
+ if (shouldAttach(ownership.concepts, concept, domainKey)) {
144
+ pushUnique(collections.concepts, concept);
145
+ }
146
+
147
+ if (
148
+ concept.parentConcept &&
149
+ shouldAttach(ownership.concepts, concept.parentConcept, domainKey)
150
+ ) {
151
+ if (pushUnique(collections.concepts, concept.parentConcept)) {
152
+ pendingConcepts.push(concept.parentConcept);
153
+ }
154
+ }
155
+
156
+ for (const endpoint of collections.endpoints) {
157
+ if (referencesConcept(endpoint, concept)) {
158
+ includeEndpoint({
159
+ domainKey,
160
+ endpoint,
161
+ ownership,
162
+ collections,
163
+ pendingConcepts,
164
+ pendingEndpoints,
165
+ });
166
+ }
167
+ }
168
+ }
169
+
170
+ function includeEndpoint(options: {
171
+ domainKey: string;
172
+ endpoint: EndpointDef;
173
+ ownership: OwnershipMaps;
174
+ collections: DomainCollections;
175
+ pendingConcepts: ConceptDef[];
176
+ pendingEndpoints: EndpointDef[];
177
+ }) {
178
+ const { domainKey, endpoint, ownership, collections, pendingConcepts, pendingEndpoints } =
179
+ options;
180
+
181
+ if (shouldAttach(ownership.endpoints, endpoint, domainKey)) {
182
+ pushUnique(collections.endpoints, endpoint);
183
+ }
184
+
185
+ includeRule(domainKey, endpoint.governedBy ?? [], ownership, collections);
186
+
187
+ // An endpoint reaches concepts three ways and other endpoints one way, but the
188
+ // handling is identical: take what this domain owns, and queue whatever is new.
189
+ collectOwned({
190
+ items: [
191
+ ...(endpoint.queries ?? []),
192
+ ...(endpoint.mutates ?? []),
193
+ ...(endpoint.returns ?? []).map((returned) => returned.concept),
194
+ ],
195
+ owners: ownership.concepts,
196
+ collection: collections.concepts,
197
+ pending: pendingConcepts,
198
+ domainKey,
199
+ });
200
+
201
+ collectOwned({
202
+ items: (endpoint.dependsOn ?? []).map((dependency) => dependency.endpoint),
203
+ owners: ownership.endpoints,
204
+ collection: collections.endpoints,
205
+ pending: pendingEndpoints,
206
+ domainKey,
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Only what this domain owns is attached, and only the first sighting is queued —
212
+ * re-queueing something already collected would walk the graph forever.
213
+ */
214
+ function collectOwned<T extends object>(options: {
215
+ items: T[];
216
+ owners: WeakMap<T, string>;
217
+ collection: T[];
218
+ pending: T[];
219
+ domainKey: string;
220
+ }) {
221
+ const { items, owners, collection, pending, domainKey } = options;
222
+
223
+ for (const item of items) {
224
+ if (shouldAttach(owners, item, domainKey) && pushUnique(collection, item)) {
225
+ pending.push(item);
226
+ }
227
+ }
228
+ }
229
+
230
+ function includeService(options: {
231
+ domainKey: string;
232
+ service: ServiceDef;
233
+ ownership: OwnershipMaps;
234
+ collections: DomainCollections;
235
+ pendingConcepts: ConceptDef[];
236
+ }) {
237
+ const { domainKey, service, ownership, collections, pendingConcepts } = options;
238
+
239
+ if (shouldAttach(ownership.services, service, domainKey)) {
240
+ pushUnique(collections.services, service);
241
+ }
242
+
243
+ includeRule(domainKey, service.governedBy ?? [], ownership, collections);
244
+
245
+ if (
246
+ shouldAttach(ownership.concepts, service.belongsTo, domainKey) &&
247
+ pushUnique(collections.concepts, service.belongsTo)
248
+ ) {
249
+ pendingConcepts.push(service.belongsTo);
250
+ }
251
+ }
252
+
253
+ function includeRule(
254
+ domainKey: string,
255
+ rules: readonly RuleDef[],
256
+ ownership: OwnershipMaps,
257
+ collections: DomainCollections,
258
+ ) {
259
+ for (const rule of rules) {
260
+ if (shouldAttach(ownership.rules, rule, domainKey)) {
261
+ pushUnique(collections.rules, rule);
262
+ }
263
+ }
264
+ }
265
+
266
+ function shouldAttach<T extends object>(owners: WeakMap<T, string>, value: T, domainKey: string) {
267
+ const owner = owners.get(value);
268
+ return owner == null || owner === domainKey;
269
+ }
270
+
271
+ function pushUnique<T extends object>(values: T[], value: T) {
272
+ if (values.includes(value)) {
273
+ return false;
274
+ }
275
+
276
+ values.push(value);
277
+ return true;
278
+ }
279
+
280
+ function referencesConcept(endpoint: EndpointDef, concept: ConceptDef) {
281
+ return (
282
+ endpoint.queries?.includes(concept) ||
283
+ endpoint.mutates?.includes(concept) ||
284
+ endpoint.returns?.some(function (returned) {
285
+ return returned.concept === concept;
286
+ })
287
+ );
288
+ }