@m6d/cortex-cli 0.1.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 (61) hide show
  1. package/README.md +175 -0
  2. package/bin/cortex.js +52 -0
  3. package/package.json +41 -0
  4. package/src/cc/client.ts +295 -0
  5. package/src/cc/flags.ts +135 -0
  6. package/src/cc/output.ts +77 -0
  7. package/src/cc/profiles.ts +105 -0
  8. package/src/cli.ts +56 -0
  9. package/src/commands/cc.ts +504 -0
  10. package/src/commands/graph.ts +229 -0
  11. package/src/commands/new.ts +333 -0
  12. package/src/commands/swagger.ts +261 -0
  13. package/src/config/load.ts +122 -0
  14. package/src/config/validate.ts +46 -0
  15. package/src/contracts/charts/chart-spec.ts +201 -0
  16. package/src/contracts/charts/index.ts +7 -0
  17. package/src/contracts/client-tools/index.ts +82 -0
  18. package/src/contracts/egress/index.ts +161 -0
  19. package/src/contracts/egress/scrub.ts +59 -0
  20. package/src/contracts/graph/clients/embed.ts +45 -0
  21. package/src/contracts/graph/clients/neo4j.ts +85 -0
  22. package/src/contracts/graph/helpers.ts +57 -0
  23. package/src/contracts/graph/index.ts +30 -0
  24. package/src/contracts/graph/schema.ts +65 -0
  25. package/src/contracts/graph/types.ts +125 -0
  26. package/src/contracts/markdown/index.ts +39 -0
  27. package/src/contracts/rich-text/index.ts +220 -0
  28. package/src/contracts/runtime/api.ts +328 -0
  29. package/src/contracts/runtime/auth.ts +41 -0
  30. package/src/contracts/runtime/dataset-sql-guide.ts +67 -0
  31. package/src/contracts/runtime/dataset-sql.ts +2522 -0
  32. package/src/contracts/runtime/datasets.fixtures.json +2849 -0
  33. package/src/contracts/runtime/datasets.ts +1391 -0
  34. package/src/contracts/runtime/grammar.ts +161 -0
  35. package/src/contracts/runtime/index.ts +14 -0
  36. package/src/contracts/runtime/signature.ts +237 -0
  37. package/src/contracts/runtime/sync.ts +172 -0
  38. package/src/contracts/runtime/triggers.ts +224 -0
  39. package/src/contracts/wire/index.ts +264 -0
  40. package/src/graph/expand-domains.ts +288 -0
  41. package/src/graph/generate-cypher.ts +201 -0
  42. package/src/graph/seed.ts +207 -0
  43. package/src/graph/validate.ts +78 -0
  44. package/src/scaffold/features.ts +190 -0
  45. package/src/scaffold/files.ts +411 -0
  46. package/src/scaffold/ports.ts +86 -0
  47. package/src/scaffold/templates/domains-README.md.txt +16 -0
  48. package/src/scaffold/templates/eslint.config.mjs.txt +20 -0
  49. package/src/scaffold/templates/fallowrc.jsonc.txt +42 -0
  50. package/src/scaffold/templates/gitignore.txt +19 -0
  51. package/src/scaffold/templates/index.ts.txt +18 -0
  52. package/src/scaffold/templates/prettier.config.mjs.txt +5 -0
  53. package/src/scaffold/templates/tsconfig.json.txt +25 -0
  54. package/src/swagger/extract-endpoints.ts +266 -0
  55. package/src/swagger/generated-block.ts +74 -0
  56. package/src/swagger/openapi-schema.ts +161 -0
  57. package/src/swagger/parse-response.ts +88 -0
  58. package/src/swagger/types.ts +25 -0
  59. package/src/ui/prompts.ts +125 -0
  60. package/src/ui/report.ts +107 -0
  61. package/tsconfig.json +46 -0
@@ -0,0 +1,161 @@
1
+ import { z } from "zod";
2
+ import { LOCALES, TOOL_SURFACES, type Greeting, type Suggestions } from "../wire/index";
3
+
4
+ /*
5
+ * The grammar both sides of the runtime API share: identifier patterns, the
6
+ * closed sets, and the small schemas every request and response is built from.
7
+ */
8
+
9
+ /** The agent slug path param, validated on both sides of the wire. */
10
+ export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
11
+ export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
12
+
13
+ /** Tool names become `tools.<name>()` sandbox bindings on the server. */
14
+ export const TOOL_NAME_PATTERN = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
15
+
16
+ /** Consumer-facing slugs: service keys, skill names, and trigger names share it. */
17
+ export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
18
+ export const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
19
+
20
+ export const localeSchema = z.enum(LOCALES);
21
+ export const toolSurfaceSchema = z.enum(TOOL_SURFACES);
22
+ export const catalogVersionSchema = z.string().regex(/^[0-9a-f]{16}$/);
23
+ export const sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/);
24
+
25
+ /**
26
+ * A variable the server fills per request: a session value, a request-context
27
+ * value, or one the SDK computes. Declared once per server so the console
28
+ * offers exactly what that server provides to the agents created on it, in
29
+ * prompts and as tool input sources alike.
30
+ */
31
+ export const variableDeclarationSchema = z.object({
32
+ name: z
33
+ .string()
34
+ .regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)
35
+ .max(64),
36
+ description: z.string().max(500),
37
+ example: z.string().max(200),
38
+ /** Computed by the SDK per request (`utcTime`, `locale`); no session value can set it. */
39
+ computed: z.boolean(),
40
+ });
41
+ export type VariableDeclaration = z.infer<typeof variableDeclarationSchema>;
42
+
43
+ /** Variable names a tool input or prompt reads. */
44
+ export const boundVariablesSchema = z.array(variableDeclarationSchema.shape.name);
45
+
46
+ /** A sandbox binding a piece of rich text mentions, so the turn can register it before the model reads the text. */
47
+ export const mentionedToolSchema = z.object({
48
+ toolId: z.uuid(),
49
+ name: z.string().regex(TOOL_NAME_PATTERN),
50
+ readOnly: z.boolean(),
51
+ /** A tool that parks for approval is never a sandbox binding, mentioned or not. */
52
+ requiresApproval: z.boolean(),
53
+ variables: boundVariablesSchema,
54
+ });
55
+
56
+ /**
57
+ * Variables the SDK itself fills for every agent, listed first in a server's
58
+ * declaration so the console offers them like any consumer-declared one.
59
+ */
60
+ export const SDK_VARIABLES = [
61
+ {
62
+ name: "utcTime",
63
+ description: "Current time in UTC (ISO 8601)",
64
+ example: "2026-09-12T08:30:00Z",
65
+ computed: true,
66
+ },
67
+ {
68
+ name: "locale",
69
+ description: "Resolved locale of the session",
70
+ example: "ar",
71
+ computed: true,
72
+ },
73
+ {
74
+ name: "timezone",
75
+ description: "The user's IANA time zone, as the widget reports it",
76
+ example: "Asia/Riyadh",
77
+ computed: true,
78
+ },
79
+ ] as const satisfies ReadonlyArray<VariableDeclaration>;
80
+
81
+ /**
82
+ * The widget's landing-page prompt chips, per locale: a short title the chip
83
+ * shows and the prompt it sends when clicked. Every source of them
84
+ * (the Control Center agent row, a server-code override, the capabilities
85
+ * handshake) stores this exact shape, so the widget never sees a blank or
86
+ * unbounded chip whichever source won. Pure validation, no transforms: a
87
+ * value that passes is served verbatim, so each source trims before it
88
+ * stores.
89
+ */
90
+ export const SUGGESTION_LIMITS = { perLocale: 6, titleLength: 120, promptLength: 2000 } as const;
91
+ const trimmedText = (max: number) =>
92
+ z
93
+ .string()
94
+ .min(1)
95
+ .max(max)
96
+ .refine((text) => text === text.trim(), "text has surrounding whitespace");
97
+ export const suggestionsSchema = z.record(
98
+ localeSchema,
99
+ z
100
+ .array(
101
+ z.object({
102
+ title: trimmedText(SUGGESTION_LIMITS.titleLength),
103
+ prompt: trimmedText(SUGGESTION_LIMITS.promptLength),
104
+ }),
105
+ )
106
+ .max(SUGGESTION_LIMITS.perLocale),
107
+ ) satisfies z.ZodType<Suggestions>;
108
+
109
+ /**
110
+ * The blank-chat landing's headline per locale. Empty means the widget's
111
+ * built-in default for that locale; like suggestions, served verbatim.
112
+ */
113
+ export const GREETING_MAX_LENGTH = 200;
114
+ /** One string per locale, at most 500 characters each; author copy the widget shows as is. */
115
+ export const localizedTextSchema = z.record(localeSchema, z.string().max(500));
116
+
117
+ export const greetingSchema = z.record(
118
+ localeSchema,
119
+ z
120
+ .string()
121
+ .max(GREETING_MAX_LENGTH)
122
+ .refine((text) => text === text.trim(), "greeting has surrounding whitespace"),
123
+ ) satisfies z.ZodType<Greeting>;
124
+
125
+ /**
126
+ * The Control Center's id for a resolved agent. Servers echo it on reports
127
+ * about that agent so they attach to it even if the slug or alias they used
128
+ * later moves.
129
+ */
130
+ export const agentRefSchema = z.uuid();
131
+
132
+ /**
133
+ * Response header carrying the resolved agent's ref on every agent-scoped
134
+ * runtime answer, errors included, so a server can attribute a request even
135
+ * when the body is an error envelope. Absent only when no agent resolved.
136
+ */
137
+ export const AGENT_REF_HEADER = "X-CC-Agent-Ref";
138
+
139
+ /**
140
+ * The playground operator's stand-in for a test thread's session: a
141
+ * URI-encoded JSON object of declared variable values, sent by cc's
142
+ * playground beside `X-Cortex-Playground` and honoured only when that header
143
+ * verifies. The server stores it as the thread's session on creation, so the
144
+ * customer's `loadSessionData` (which never sees cc-minted tokens) is not
145
+ * needed for prompts and tool sources to have values.
146
+ */
147
+ export const PLAYGROUND_SESSION_HEADER = "X-Cortex-Playground-Session";
148
+
149
+ /** Sent by cc's playground; a server verifies it against cc's JWKS before honouring it. */
150
+ export const PLAYGROUND_HEADER = "X-Cortex-Playground";
151
+
152
+ /** The end user's bearer, forwarded verbatim by the server to cc and by cc to the tool's backend. */
153
+ export const END_USER_TOKEN_HEADER = "X-End-User-Token";
154
+
155
+ /** Execute idempotency: a server-minted key the runtime replays within its window. */
156
+ export const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
157
+ export const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
158
+
159
+ /** Correlation id echoed on every runtime response. */
160
+ export const REQUEST_ID_HEADER = "X-Request-Id";
161
+ export const REQUEST_ID_MAX_LENGTH = 128;
@@ -0,0 +1,14 @@
1
+ /*
2
+ * The contract between the cortex-cc console and `@m6d/cortex-server`:
3
+ * the grammar they share, the runtime API cc serves, triggers in both
4
+ * directions, and the fleet sync the server pushes.
5
+ */
6
+ export * from "./grammar";
7
+ export * from "./auth";
8
+ export * from "./api";
9
+ export * from "./triggers";
10
+ export * from "./datasets";
11
+ export * from "./dataset-sql";
12
+ export * from "./dataset-sql-guide";
13
+ export * from "./sync";
14
+ export * from "./signature";
@@ -0,0 +1,237 @@
1
+ /*
2
+ * The one rendering of a dynamic tool the model sees, whichever side
3
+ * produced it: Control Center renders its registry tools at resolve time,
4
+ * `@m6d/cortex-server` renders an MCP server's tools at turn time, and both
5
+ * land in the same "Dynamic Tools" prompt section. A TypeScript-like
6
+ * signature over the tool's JSON Schema: the input typed field by field with
7
+ * its description as a trailing comment, the response as the names of the
8
+ * fields it carries (what a script reads off the result), never their types.
9
+ */
10
+
11
+ /** Past this nesting an object prints as `object`: deeper detail costs more prompt than it earns. */
12
+ const MAX_DEPTH = 3;
13
+
14
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
15
+
16
+ function isRecord(value: unknown): value is Record<string, unknown> {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ function propertyName(name: string) {
21
+ return IDENTIFIER.test(name) ? name : JSON.stringify(name);
22
+ }
23
+
24
+ function compactLine(value: string) {
25
+ return value.replace(/\s+/g, " ").trim();
26
+ }
27
+
28
+ function fieldComment(value: unknown) {
29
+ if (typeof value !== "string") return "";
30
+ return Array.from(compactLine(value)).slice(0, 80).join("");
31
+ }
32
+
33
+ /** The `required` names of an object schema, or none when it declares none. */
34
+ export function requiredProperties(schema: unknown) {
35
+ return isRecord(schema) && Array.isArray(schema.required)
36
+ ? schema.required.filter((name): name is string => typeof name === "string")
37
+ : [];
38
+ }
39
+
40
+ /** Property names in the schema's `x-order` first, then the rest as declared. */
41
+ export function orderedPropertyNames(schema: Record<string, unknown>) {
42
+ const properties = isRecord(schema.properties) ? schema.properties : {};
43
+ const order = Array.isArray(schema["x-order"])
44
+ ? schema["x-order"].filter(
45
+ (name): name is string => typeof name === "string" && name in properties,
46
+ )
47
+ : [];
48
+ return [...order, ...Object.keys(properties).filter((name) => !order.includes(name))];
49
+ }
50
+
51
+ const SCALAR_TYPES = new Set(["boolean", "number", "string"]);
52
+
53
+ /**
54
+ * Follows a local `$ref` (`#`, `#/$defs/x`, or `#/definitions/x`) to its
55
+ * definition in the root document, keeping the keywords beside the ref (a
56
+ * description, say), so a shared shape a converter hoisted (a tool's schema
57
+ * rows) renders as its fields rather than `unknown`. A described ref comes out
58
+ * of zod as a one-entry `allOf`, which reads the same. A recursive shape ends
59
+ * where `MAX_DEPTH` ends it.
60
+ */
61
+ function resolver(root: unknown) {
62
+ function definition(pointer: string) {
63
+ if (pointer === "#") return root;
64
+ const parts = pointer.split("/");
65
+ const [, section, name] = parts;
66
+ const definitions =
67
+ isRecord(root) && (section === "$defs" || section === "definitions")
68
+ ? root[section]
69
+ : undefined;
70
+ return parts.length === 3 && isRecord(definitions) && name !== undefined
71
+ ? definitions[name]
72
+ : undefined;
73
+ }
74
+ return function resolve(schema: unknown): unknown {
75
+ if (!isRecord(schema)) return schema;
76
+ const { $ref, allOf, ...rest } = schema;
77
+ if (typeof $ref === "string") {
78
+ const target = definition($ref);
79
+ return isRecord(target) ? { ...target, ...rest } : target;
80
+ }
81
+ if (Array.isArray(allOf) && allOf.length === 1) {
82
+ const target = resolve(allOf[0]);
83
+ return isRecord(target) ? { ...target, ...rest } : target;
84
+ }
85
+ return schema;
86
+ };
87
+ }
88
+
89
+ type Resolve = ReturnType<typeof resolver>;
90
+
91
+ function literalUnion(values: unknown[]) {
92
+ return values
93
+ .filter((value) => SCALAR_TYPES.has(typeof value))
94
+ .map((value) => JSON.stringify(value))
95
+ .join(" | ");
96
+ }
97
+
98
+ /** An object's fields one per line, each typed, marked optional, and commented from its description. */
99
+ function inputObject(
100
+ schema: Record<string, unknown>,
101
+ resolve: Resolve,
102
+ depth: number,
103
+ indent: number,
104
+ ) {
105
+ if (depth >= MAX_DEPTH) return "object";
106
+ const properties = isRecord(schema.properties) ? schema.properties : {};
107
+ const required = new Set(requiredProperties(schema));
108
+ const fields = orderedPropertyNames(schema).map((name) => {
109
+ const child = resolve(properties[name]);
110
+ const comment = isRecord(child) ? fieldComment(child.description) : "";
111
+ return `${" ".repeat(indent + 2)}${propertyName(name)}${required.has(name) ? "" : "?"}: ${inputType(child, resolve, depth + 1, indent + 2)},${comment ? ` // ${comment}` : ""}`;
112
+ });
113
+ return fields.length > 0 ? `{\n${fields.join("\n")}\n${" ".repeat(indent)}}` : "{}";
114
+ }
115
+
116
+ function inputType(schema: unknown, resolve: Resolve, depth = 0, indent = 0): string {
117
+ schema = resolve(schema);
118
+ if (!isRecord(schema)) return "unknown";
119
+ if ("const" in schema && SCALAR_TYPES.has(typeof schema.const)) {
120
+ return JSON.stringify(schema.const);
121
+ }
122
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) return literalUnion(schema.enum);
123
+ const variants = variantsOf(schema);
124
+ if (variants) {
125
+ const rendered = variants.map((variant) => inputType(variant, resolve, depth, indent));
126
+ return [...new Set(rendered)].join(" | ");
127
+ }
128
+ if (schema.type === "null") return "null";
129
+ if (schema.type === "array") {
130
+ return `${inputType(schema.items, resolve, depth + 1, indent)}[]`;
131
+ }
132
+ if (schema.type === "object" || isRecord(schema.properties)) {
133
+ return inputObject(schema, resolve, depth, indent);
134
+ }
135
+ if (schema.type === "integer") return "number";
136
+ return SCALAR_TYPES.has(String(schema.type)) ? String(schema.type) : "unknown";
137
+ }
138
+
139
+ /** The branches of a union schema, under whichever keyword carries them. */
140
+ function variantsOf(schema: Record<string, unknown>) {
141
+ const variants = [schema.oneOf, schema.anyOf].find(Array.isArray);
142
+ return variants && variants.length > 0 ? variants : undefined;
143
+ }
144
+
145
+ function isObjectSchema(schema: unknown): schema is Record<string, unknown> {
146
+ return isRecord(schema) && (schema.type === "object" || isRecord(schema.properties));
147
+ }
148
+
149
+ /**
150
+ * A response schema as one shape: a union's object branches merged into the
151
+ * fields any of them may carry, a `null` branch dropped. A script reads a
152
+ * field off whichever branch came back, so the names are what matter.
153
+ */
154
+ function responseUnion(schema: unknown, resolve: Resolve): unknown {
155
+ const resolved = resolve(schema);
156
+ if (!isRecord(resolved)) return resolved;
157
+ const variants = variantsOf(resolved);
158
+ if (!variants) return resolved;
159
+ const branches = variants
160
+ .map((variant) => responseUnion(variant, resolve))
161
+ .filter(
162
+ (variant): variant is Record<string, unknown> =>
163
+ isRecord(variant) && variant.type !== "null",
164
+ );
165
+ const objects = branches.filter(isObjectSchema);
166
+ if (objects.length === 0) return branches[0];
167
+ const properties: Record<string, unknown> = {};
168
+ for (const object of objects) {
169
+ if (isRecord(object.properties)) Object.assign(properties, object.properties);
170
+ }
171
+ return { type: "object", properties };
172
+ }
173
+
174
+ function responseObject(schema: Record<string, unknown>, resolve: Resolve, depth = 0): string {
175
+ const properties = isRecord(schema.properties) ? schema.properties : {};
176
+ const fields = orderedPropertyNames(schema).map((name) => {
177
+ const child = responseUnion(properties[name], resolve);
178
+ if (!isRecord(child) || depth >= MAX_DEPTH) return propertyName(name);
179
+ if (child.type === "array") {
180
+ const items = responseUnion(child.items, resolve);
181
+ return isObjectSchema(items)
182
+ ? `${propertyName(name)}[].${responseObject(items, resolve, depth + 1)}`
183
+ : propertyName(name);
184
+ }
185
+ return isObjectSchema(child)
186
+ ? `${propertyName(name)}.${responseObject(child, resolve, depth + 1)}`
187
+ : propertyName(name);
188
+ });
189
+ return fields.length > 0 ? `{ ${fields.join(", ")} }` : "{}";
190
+ }
191
+
192
+ /**
193
+ * The names a result carries, as a script would read them: `{ id, items[].{ name } }`.
194
+ * A schema that describes no fields is `{}`, which is also what a tool that
195
+ * declares no response gets; a caller that knows nothing passes `undefined`
196
+ * and gets `unknown`, so the model does not mistake silence for emptiness.
197
+ */
198
+ export function responseShape(schema: unknown) {
199
+ if (schema === undefined) return "unknown";
200
+ const resolve = resolver(schema);
201
+ const root = responseUnion(schema, resolve);
202
+ if (!isRecord(root)) return "{}";
203
+ if (root.type === "array") {
204
+ const items = responseUnion(root.items, resolve);
205
+ return isObjectSchema(items) ? `[]${responseObject(items, resolve)}` : "[]";
206
+ }
207
+ return responseObject(root, resolve);
208
+ }
209
+
210
+ /** One line, no comment terminator, and always a sentence, so it can sit in the doc comment. */
211
+ function summarize(description: string) {
212
+ const text = compactLine(description).replaceAll("*/", "* /");
213
+ if (!text) return "";
214
+ return /[.!?]$/.test(text) ? text : `${text}.`;
215
+ }
216
+
217
+ export type ToolSignatureInput = {
218
+ /** As the script calls it: a name dot access cannot reach renders as the index call `tools["a-b"]`. */
219
+ name: string;
220
+ description: string;
221
+ /** JSON Schema of the input object as the model should see it. */
222
+ inputSchema: unknown;
223
+ /** Already rendered (a shared-shape ref like `$R1`), or the JSON Schema to render, or undefined for "unknown". */
224
+ returns: string | { schema: unknown };
225
+ /** Appended to the summary; the caller knows what else the model should be told (idempotency, embedded flows). */
226
+ notes?: ReadonlyArray<string>;
227
+ };
228
+
229
+ /** `/** summary *\/\nname(input: {...}): { fields }`. */
230
+ export function renderToolSignature(tool: ToolSignatureInput) {
231
+ const summary = [summarize(tool.description), ...(tool.notes ?? [])].filter(Boolean).join(" ");
232
+ const callee = IDENTIFIER.test(tool.name) ? tool.name : `tools[${JSON.stringify(tool.name)}]`;
233
+ const returns =
234
+ typeof tool.returns === "string" ? tool.returns : responseShape(tool.returns.schema);
235
+ const input = inputType(tool.inputSchema, resolver(tool.inputSchema));
236
+ return `${summary ? `/** ${summary} */\n` : ""}${callee}(input: ${input}): ${returns}`;
237
+ }
@@ -0,0 +1,172 @@
1
+ import { z } from "zod";
2
+ import { MESSAGE_ROLES, type TokenUsage } from "../wire/index";
3
+ import { agentRefSchema, agentSlugSchema, variableDeclarationSchema } from "./grammar";
4
+ import { TRIGGER_OUTCOMES, TRIGGER_SOURCES } from "./triggers";
5
+ import { DATASET_ERROR_KINDS } from "./datasets";
6
+
7
+ /*
8
+ * Fleet sync: `@m6d/cortex-server` mirrors thread/message/usage activity into
9
+ * cc via `POST /api/runtime/sync` batches. Upserts are idempotent on
10
+ * (serverId, id); an empty batch is the heartbeat.
11
+ */
12
+
13
+ const tokenCountSchema = z.number().int().nonnegative();
14
+
15
+ export const tokenUsageSchema = z.object({
16
+ input: z.object({
17
+ noCache: tokenCountSchema,
18
+ cacheRead: tokenCountSchema,
19
+ cacheWrite: tokenCountSchema,
20
+ total: tokenCountSchema,
21
+ }),
22
+ output: z.object({
23
+ reasoning: tokenCountSchema,
24
+ text: tokenCountSchema,
25
+ total: tokenCountSchema,
26
+ }),
27
+ total: tokenCountSchema,
28
+ }) satisfies z.ZodType<TokenUsage>;
29
+
30
+ /**
31
+ * What a server offers every agent the Control Center creates for it, sent
32
+ * whole at boot in `server.declare`: the `{{variables}}` its session loader
33
+ * and request context fill, SDK ones included. Agents in the server's own
34
+ * code never appear here; the Control Center is blind to them.
35
+ */
36
+ export const serverDeclarationSchema = z.object({
37
+ variables: z.array(variableDeclarationSchema),
38
+ });
39
+ export type ServerDeclaration = z.infer<typeof serverDeclarationSchema>;
40
+
41
+ export const syncEventSchema = z.discriminatedUnion("type", [
42
+ z.object({
43
+ type: z.literal("thread.upsert"),
44
+ id: z.uuid(),
45
+ agentSlug: agentSlugSchema,
46
+ userId: z.string().max(255),
47
+ title: z.string().max(256).nullable(),
48
+ isTest: z.boolean(),
49
+ /** Set on headless threads a trigger owns; `userId` is then a sentinel. */
50
+ triggerName: z.string().max(64).nullable(),
51
+ triggerKey: z.string().max(256).nullable(),
52
+ createdAt: z.iso.datetime(),
53
+ updatedAt: z.iso.datetime(),
54
+ }),
55
+ z.object({
56
+ type: z.literal("thread.delete"),
57
+ id: z.uuid(),
58
+ }),
59
+ z.object({
60
+ type: z.literal("message.upsert"),
61
+ id: z.string().max(128),
62
+ threadId: z.uuid(),
63
+ role: z.enum(MESSAGE_ROLES),
64
+ text: z.string().nullable(),
65
+ ordinal: z.number().int(),
66
+ content: z.looseObject({}),
67
+ createdAt: z.iso.datetime(),
68
+ }),
69
+ z.object({
70
+ type: z.literal("usage.record"),
71
+ id: z.uuid(),
72
+ messageId: z.string().max(128),
73
+ threadId: z.uuid(),
74
+ stepNumber: z.number().int(),
75
+ tokenUsage: tokenUsageSchema,
76
+ /** Stamped by the event constructor: llm_requests has no timestamp column, but time-bucketed usage stats need one. */
77
+ createdAt: z.iso.datetime(),
78
+ }),
79
+ /** One loadSkill call, hit or miss, from either skill source. Bounded by construction (no body travels). */
80
+ z.object({
81
+ type: z.literal("skill.load"),
82
+ id: z.uuid(),
83
+ threadId: z.uuid(),
84
+ agentSlug: agentSlugSchema,
85
+ /** The ref cc issued for the agent that answered; a code-only agent or a miss has none. */
86
+ agentRef: agentRefSchema.optional(),
87
+ /** As the model asked for it, even when nothing answered. */
88
+ skillName: z.string().max(64),
89
+ source: z.enum(TRIGGER_SOURCES).nullable(),
90
+ hit: z.boolean(),
91
+ /** The user message this turn answers; the mirror joins it for the drill-down. */
92
+ messageId: z.string().max(128),
93
+ createdAt: z.iso.datetime(),
94
+ }),
95
+ /** One dataset read, refused or answered; never the rows. */
96
+ z.object({
97
+ type: z.literal("dataset.read"),
98
+ id: z.uuid(),
99
+ threadId: z.uuid(),
100
+ agentSlug: agentSlugSchema,
101
+ agentRef: agentRefSchema.optional(),
102
+ host: z.string().max(64),
103
+ /** Empty when the statement was refused before it named one. */
104
+ dataset: z.string().max(64),
105
+ rowCount: z.number().int().nonnegative(),
106
+ truncated: z.boolean(),
107
+ durationMs: z.number().int().nonnegative(),
108
+ status: z.enum(["ok", ...DATASET_ERROR_KINDS]),
109
+ /** The SQL the model wrote, when the read came from a statement. */
110
+ sql: z.string().max(4000).optional(),
111
+ /** Why a read did not answer: the parser's refusal, the validator's issues or the host's detail. */
112
+ detail: z.string().max(1000).optional(),
113
+ /** The user or trigger message this turn answers; the mirror joins it for the drill-down. */
114
+ messageId: z.string().max(128),
115
+ createdAt: z.iso.datetime(),
116
+ }),
117
+ /** One firing, upserted on start and again on its terminal outcome; the firing id is the event id so both land on one mirror row. */
118
+ z.object({
119
+ type: z.literal("trigger.firing"),
120
+ id: z.uuid(),
121
+ agentSlug: agentSlugSchema,
122
+ agentRef: agentRefSchema.optional(),
123
+ triggerName: z.string().max(64),
124
+ source: z.enum(TRIGGER_SOURCES),
125
+ threadId: z.uuid().nullable(),
126
+ runId: z.string().max(64).nullable(),
127
+ outcome: z.enum(TRIGGER_OUTCOMES),
128
+ reason: z.string().max(64).nullable(),
129
+ output: z.unknown(),
130
+ createdAt: z.iso.datetime(),
131
+ finishedAt: z.iso.datetime().nullable(),
132
+ }),
133
+ /** What the server offers console-created agents, sent whole at boot and replacing the previous declaration. */
134
+ z.object({
135
+ type: z.literal("server.declare"),
136
+ id: z.uuid(),
137
+ ...serverDeclarationSchema.shape,
138
+ createdAt: z.iso.datetime(),
139
+ }),
140
+ ]);
141
+ export type SyncEvent = z.infer<typeof syncEventSchema>;
142
+
143
+ /*
144
+ * `epoch` identifies the sending database's generation (a UUIDv7 minted into
145
+ * the server DB's `sync_meta` row): a recreated database gets a new epoch, so
146
+ * cc can tell a legitimately restarted outbox sequence from a stale batch.
147
+ * v7 uuids are time-ordered, so cc also rejects batches from a superseded
148
+ * (lexicographically lower) epoch outright.
149
+ * `seq` is the batch's highest outbox row id; batches are contiguous
150
+ * id-ordered outbox prefixes, so within one epoch cc keeps a per-server
151
+ * high-water mark and discards batches at or below it. A stale in-flight
152
+ * batch (an expired flusher lease, a replay after a lost ack) can never
153
+ * overwrite newer mirrored state. Heartbeats carry no events and nothing to
154
+ * fence, so `seq` exists only on the non-empty branch.
155
+ */
156
+ export const syncRequestSchema = z.union([
157
+ z.object({
158
+ events: z.array(syncEventSchema).min(1).max(500),
159
+ epoch: z.uuid(),
160
+ seq: z.number().int().positive(),
161
+ }),
162
+ z.object({
163
+ events: z.array(syncEventSchema).length(0),
164
+ epoch: z.uuid(),
165
+ }),
166
+ ]);
167
+ export type SyncRequest = z.input<typeof syncRequestSchema>;
168
+
169
+ export const syncResponseSchema = z.object({
170
+ accepted: z.number().int(),
171
+ });
172
+ export type SyncResponse = z.infer<typeof syncResponseSchema>;