@m6d/cortex-cli 0.12.1 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +193 -128
  2. package/package.json +16 -5
  3. package/src/cli.ts +19 -12
  4. package/src/commands/apply.ts +186 -0
  5. package/src/commands/{cc.ts → management.ts} +173 -112
  6. package/src/commands/new.ts +83 -84
  7. package/src/commands/setup.ts +610 -0
  8. package/src/contracts/charts/chart-spec.ts +1 -1
  9. package/src/contracts/client-tools/index.ts +3 -3
  10. package/src/contracts/{runtime → datasets}/datasets.ts +7 -42
  11. package/src/contracts/datasets/index.ts +3 -0
  12. package/src/contracts/egress-policy/index.ts +16 -0
  13. package/src/contracts/{egress → egress-policy}/scrub.ts +34 -5
  14. package/src/contracts/{egress → egress-policy}/url.ts +22 -9
  15. package/src/contracts/errors/index.ts +38 -0
  16. package/src/contracts/json/index.ts +5 -0
  17. package/src/contracts/manifest/index.ts +973 -0
  18. package/src/contracts/manifest-file/index.ts +16 -0
  19. package/src/contracts/markdown/index.ts +1 -1
  20. package/src/contracts/pattern/index.ts +276 -0
  21. package/src/contracts/server/index.ts +349 -0
  22. package/src/contracts/server/reserved-names.fixtures.json +18 -0
  23. package/src/contracts/triggers/index.ts +112 -0
  24. package/src/contracts/wire/index.ts +67 -33
  25. package/src/{cc → management}/client.ts +33 -7
  26. package/src/{cc → management}/flags.ts +2 -1
  27. package/src/{cc → management}/profiles.ts +10 -8
  28. package/src/scaffold/files.ts +448 -284
  29. package/src/scaffold/ports.ts +12 -24
  30. package/src/scaffold/templates/cspell.json.txt +41 -0
  31. package/src/scaffold/templates/datasets.ts.txt +52 -0
  32. package/src/scaffold/templates/dockerignore.txt +6 -0
  33. package/src/scaffold/templates/fallowrc.jsonc.txt +8 -12
  34. package/src/scaffold/templates/gitignore.txt +1 -1
  35. package/src/scaffold/templates/server.ts.txt +40 -0
  36. package/src/scaffold/templates/setup.ts.txt +44 -0
  37. package/src/ui/prompts.ts +24 -38
  38. package/src/ui/report.ts +5 -15
  39. package/tsconfig.json +2 -3
  40. package/src/commands/graph.ts +0 -228
  41. package/src/commands/swagger.ts +0 -247
  42. package/src/config/load.ts +0 -122
  43. package/src/config/validate.ts +0 -46
  44. package/src/contracts/credentials/index.ts +0 -183
  45. package/src/contracts/egress/index.ts +0 -120
  46. package/src/contracts/graph/clients/embed.ts +0 -62
  47. package/src/contracts/graph/clients/neo4j.ts +0 -85
  48. package/src/contracts/graph/helpers.ts +0 -55
  49. package/src/contracts/graph/index.ts +0 -30
  50. package/src/contracts/graph/schema.ts +0 -65
  51. package/src/contracts/graph/types.ts +0 -125
  52. package/src/contracts/rich-text/index.ts +0 -220
  53. package/src/contracts/runtime/api.ts +0 -345
  54. package/src/contracts/runtime/auth.ts +0 -41
  55. package/src/contracts/runtime/grammar.ts +0 -169
  56. package/src/contracts/runtime/index.ts +0 -15
  57. package/src/contracts/runtime/models.ts +0 -50
  58. package/src/contracts/runtime/signature.ts +0 -255
  59. package/src/contracts/runtime/sync.ts +0 -196
  60. package/src/contracts/runtime/triggers.ts +0 -267
  61. package/src/graph/expand-domains.ts +0 -286
  62. package/src/graph/generate-cypher.ts +0 -201
  63. package/src/graph/seed.ts +0 -199
  64. package/src/graph/validate.ts +0 -78
  65. package/src/scaffold/features.ts +0 -190
  66. package/src/scaffold/templates/domains-README.md.txt +0 -16
  67. package/src/scaffold/templates/index.ts.txt +0 -18
  68. package/src/swagger/extract-endpoints.ts +0 -269
  69. package/src/swagger/generated-block.ts +0 -74
  70. package/src/swagger/openapi-schema.ts +0 -168
  71. package/src/swagger/parse-response.ts +0 -88
  72. package/src/swagger/types.ts +0 -25
  73. /package/src/contracts/{runtime → datasets}/dataset-sql-guide.ts +0 -0
  74. /package/src/contracts/{runtime → datasets}/dataset-sql.ts +0 -0
  75. /package/src/contracts/{runtime → datasets}/datasets.fixtures.json +0 -0
  76. /package/src/{cc → management}/output.ts +0 -0
@@ -1,41 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /*
4
- * How a server, or one agent on it, verifies the end user's token. One union
5
- * for every reader: a code config, an agent the Control Center created, and
6
- * the runtime config cc publishes. A `custom` verifier exists in code only,
7
- * so it is not part of this contract.
8
- */
9
-
10
- export const AUTH_JWT_SECRET_ALGORITHMS = ["HS256", "HS384", "HS512"] as const;
11
-
12
- const issuerSchema = z.string().trim().min(1).max(2048);
13
- /** When set, the token's `aud` must contain it, so a token minted for another audience under the same issuer cannot be replayed here. */
14
- const audienceSchema = z.string().trim().min(1).max(2048).optional();
15
-
16
- /** Asymmetric tokens: the issuer publishes its public keys at the JWKS URI. */
17
- export const jwksAuthSchema = z.object({
18
- kind: z.literal("jwks"),
19
- jwksUri: z.url(),
20
- issuer: issuerSchema,
21
- audience: audienceSchema,
22
- });
23
-
24
- /**
25
- * Symmetric tokens: the issuer and this server share the secret. At least
26
- * 32 characters, the JOSE minimum for HMAC keys; the algorithms are pinned
27
- * so a token signed another way is refused before the key is tried.
28
- */
29
- export const jwtSecretAuthSchema = z.object({
30
- kind: z.literal("jwt-secret"),
31
- secret: z.string().min(32),
32
- algorithms: z.array(z.enum(AUTH_JWT_SECRET_ALGORITHMS)).min(1),
33
- issuer: issuerSchema,
34
- audience: audienceSchema,
35
- });
36
-
37
- export const publishedAuthSchema = z.discriminatedUnion("kind", [
38
- jwksAuthSchema,
39
- jwtSecretAuthSchema,
40
- ]);
41
- export type PublishedAuth = z.infer<typeof publishedAuthSchema>;
@@ -1,169 +0,0 @@
1
- import { z } from "zod";
2
- import {
3
- LOCALES,
4
- MEDIA_FORMS,
5
- TOOL_SURFACES,
6
- type Greeting,
7
- type Suggestions,
8
- } from "../wire/index";
9
-
10
- /*
11
- * The grammar both sides of the runtime API share: identifier patterns, the
12
- * closed sets, and the small schemas every request and response is built from.
13
- */
14
-
15
- /** The agent slug path param, validated on both sides of the wire. */
16
- export const AGENT_SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
17
- export const agentSlugSchema = z.string().regex(AGENT_SLUG_PATTERN);
18
-
19
- /**
20
- * An ASCII JavaScript identifier: what a tool name (a `tools.<name>()` sandbox
21
- * binding on the server) and a variable name must be, and what a rendered
22
- * signature prints bare rather than quoted.
23
- */
24
- export const IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
25
-
26
- /** Consumer-facing slugs: service keys, skill names, and trigger names share it. */
27
- export const SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
28
- export const slugSchema = z.string().regex(SLUG_PATTERN).max(64);
29
-
30
- export const localeSchema = z.enum(LOCALES);
31
- export const toolSurfaceSchema = z.enum(TOOL_SURFACES);
32
- export const mediaFormSchema = z.enum(MEDIA_FORMS);
33
- export const catalogVersionSchema = z.string().regex(/^[0-9a-f]{16}$/);
34
- export const sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/);
35
-
36
- /**
37
- * A variable the server fills per request: a session value, a request-context
38
- * value, or one the SDK computes. Declared once per server so the console
39
- * offers exactly what that server provides to the agents created on it, in
40
- * prompts and as tool input sources alike.
41
- */
42
- export const variableDeclarationSchema = z.object({
43
- name: z.string().regex(IDENTIFIER_PATTERN).max(64),
44
- description: z.string().max(500),
45
- example: z.string().max(200),
46
- /** Computed by the SDK per request (`utcTime`, `locale`); no session value can set it. */
47
- computed: z.boolean(),
48
- });
49
- export type VariableDeclaration = z.infer<typeof variableDeclarationSchema>;
50
-
51
- /** Variable names a tool input or prompt reads. */
52
- export const boundVariablesSchema = z.array(variableDeclarationSchema.shape.name);
53
-
54
- /** A sandbox binding a piece of rich text mentions, so the turn can register it before the model reads the text. */
55
- export const mentionedToolSchema = z.object({
56
- toolId: z.uuid(),
57
- name: z.string().regex(IDENTIFIER_PATTERN),
58
- readOnly: z.boolean(),
59
- /** A tool that parks for approval is never a sandbox binding, mentioned or not. */
60
- requiresApproval: z.boolean(),
61
- variables: boundVariablesSchema,
62
- });
63
-
64
- /**
65
- * Variables the SDK itself fills for every agent, listed first in a server's
66
- * declaration so the console offers them like any consumer-declared one.
67
- */
68
- export const SDK_VARIABLES = [
69
- {
70
- name: "utcTime",
71
- description: "Current time in UTC (ISO 8601)",
72
- example: "2026-09-12T08:30:00Z",
73
- computed: true,
74
- },
75
- {
76
- name: "locale",
77
- description: "Resolved locale of the session",
78
- example: "ar",
79
- computed: true,
80
- },
81
- {
82
- name: "timezone",
83
- description: "The user's IANA time zone, as the widget reports it",
84
- example: "Asia/Riyadh",
85
- computed: true,
86
- },
87
- ] as const satisfies ReadonlyArray<VariableDeclaration>;
88
-
89
- /**
90
- * The widget's landing-page prompt chips, per locale: a short title the chip
91
- * shows and the prompt it sends when clicked. Every source of them
92
- * (the Control Center agent row, a server-code override, the capabilities
93
- * handshake) stores this exact shape, so the widget never sees a blank or
94
- * unbounded chip whichever source won. Pure validation, no transforms: a
95
- * value that passes is served verbatim, so each source trims before it
96
- * stores.
97
- */
98
- export const SUGGESTION_LIMITS = { perLocale: 6, titleLength: 120, promptLength: 2000 } as const;
99
- const trimmedText = (max: number) =>
100
- z
101
- .string()
102
- .min(1)
103
- .max(max)
104
- .refine((text) => text === text.trim(), "text has surrounding whitespace");
105
- export const suggestionsSchema = z.record(
106
- localeSchema,
107
- z
108
- .array(
109
- z.object({
110
- title: trimmedText(SUGGESTION_LIMITS.titleLength),
111
- prompt: trimmedText(SUGGESTION_LIMITS.promptLength),
112
- }),
113
- )
114
- .max(SUGGESTION_LIMITS.perLocale),
115
- ) satisfies z.ZodType<Suggestions>;
116
-
117
- /** One string per locale, at most 500 characters each; author copy the widget shows as is. */
118
- export const localizedTextSchema = z.record(localeSchema, z.string().max(500));
119
-
120
- export const GREETING_MAX_LENGTH = 200;
121
- /**
122
- * The blank-chat landing's headline per locale. Empty means the widget's
123
- * built-in default for that locale; like suggestions, served verbatim.
124
- */
125
- export const greetingSchema = z.record(
126
- localeSchema,
127
- z
128
- .string()
129
- .max(GREETING_MAX_LENGTH)
130
- .refine((text) => text === text.trim(), "greeting has surrounding whitespace"),
131
- ) satisfies z.ZodType<Greeting>;
132
-
133
- /**
134
- * The Control Center's id for a resolved agent. Servers echo it on reports
135
- * about that agent so they attach to it even if the slug or alias they used
136
- * later moves.
137
- */
138
- export const agentRefSchema = z.uuid();
139
-
140
- /**
141
- * Response header carrying the resolved agent's ref on every agent-scoped
142
- * runtime answer, errors included, so a server can attribute a request even
143
- * when the body is an error envelope. Absent only when no agent resolved.
144
- */
145
- export const AGENT_REF_HEADER = "X-CC-Agent-Ref";
146
-
147
- /**
148
- * The playground operator's stand-in for a test thread's session: a
149
- * URI-encoded JSON object of declared variable values, sent by cc's
150
- * playground beside `X-Cortex-Playground` and honoured only when that header
151
- * verifies. The server stores it as the thread's session on creation, so the
152
- * customer's `loadSessionData` (which never sees cc-minted tokens) is not
153
- * needed for prompts and tool sources to have values.
154
- */
155
- export const PLAYGROUND_SESSION_HEADER = "X-Cortex-Playground-Session";
156
-
157
- /** Sent by cc's playground; a server verifies it against cc's JWKS before honouring it. */
158
- export const PLAYGROUND_HEADER = "X-Cortex-Playground";
159
-
160
- /** The end user's bearer, forwarded verbatim by the server to cc and by cc to the tool's backend. */
161
- export const END_USER_TOKEN_HEADER = "X-End-User-Token";
162
-
163
- /** Execute idempotency: a server-minted key the runtime replays within its window. */
164
- export const IDEMPOTENCY_KEY_HEADER = "Idempotency-Key";
165
- export const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9._:-]{1,200}$/;
166
-
167
- /** Correlation id echoed on every runtime response. */
168
- export const REQUEST_ID_HEADER = "X-Request-Id";
169
- export const REQUEST_ID_MAX_LENGTH = 128;
@@ -1,15 +0,0 @@
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 "./models";
11
- export * from "./datasets";
12
- export * from "./dataset-sql";
13
- export * from "./dataset-sql-guide";
14
- export * from "./sync";
15
- export * from "./signature";
@@ -1,50 +0,0 @@
1
- import { z } from "zod";
2
-
3
- /**
4
- * One level's pick of what a run talks to: a model a Control Center
5
- * provider lists, the reasoning effort sent with it, both, or neither. An
6
- * unset part inherits from the level above (trigger, then agent, then
7
- * server, then the server's code config). An effort carries down only as
8
- * far as the level that picked the model, and is sent only when that
9
- * model's provider lists it.
10
- */
11
- export const modelChoiceSchema = z
12
- .object({
13
- provider: z.string().min(1).nullable(),
14
- model: z.string().min(1).nullable(),
15
- effort: z.string().min(1).nullable(),
16
- })
17
- .refine((choice) => (choice.provider === null) === (choice.model === null), {
18
- error: "A provider and its model are picked together.",
19
- });
20
- export type ModelChoice = z.infer<typeof modelChoiceSchema>;
21
-
22
- /** A pick that inherits everything. */
23
- export const INHERIT_MODEL = {
24
- provider: null,
25
- model: null,
26
- effort: null,
27
- } satisfies ModelChoice;
28
-
29
- /**
30
- * An OpenAI-compatible endpoint as a server calls it: cc resolves the key
31
- * from the provider's secret, and `efforts` are the values it accepts as
32
- * `reasoning_effort`.
33
- */
34
- export const modelProviderEndpointSchema = z.object({
35
- baseURL: z.url(),
36
- apiKey: z.string().min(1),
37
- efforts: z.array(z.string().min(1)),
38
- });
39
-
40
- /**
41
- * The model picks a server needs to run one agent: its server's and the
42
- * agent's own (each trigger carries its own on its declaration), plus every
43
- * provider any of them names.
44
- */
45
- export const runtimeModelsSchema = z.object({
46
- providers: z.record(z.string(), modelProviderEndpointSchema),
47
- server: modelChoiceSchema,
48
- agent: modelChoiceSchema,
49
- });
50
- export type RuntimeModels = z.infer<typeof runtimeModelsSchema>;
@@ -1,255 +0,0 @@
1
- import { isRecord } from "../json";
2
- import { IDENTIFIER_PATTERN } from "./grammar";
3
-
4
- /*
5
- * The one rendering of a dynamic tool the model sees, whichever side
6
- * produced it: Control Center renders its registry tools at resolve time,
7
- * `@m6d/cortex-server` renders an MCP server's tools at turn time, and both
8
- * land in the same "Dynamic Tools" prompt section. A TypeScript-like
9
- * signature over the tool's JSON Schema: the input typed field by field with
10
- * its description as a trailing comment, the response as the names of the
11
- * fields it carries (what a script reads off the result), never their types.
12
- */
13
-
14
- /** Past this nesting an object prints as `object`: deeper detail costs more prompt than it earns. */
15
- const MAX_DEPTH = 3;
16
-
17
- function propertyName(name: string) {
18
- return IDENTIFIER_PATTERN.test(name) ? name : JSON.stringify(name);
19
- }
20
-
21
- function compactLine(value: string) {
22
- return value.replace(/\s+/g, " ").trim();
23
- }
24
-
25
- function fieldComment(value: unknown) {
26
- if (typeof value !== "string") return "";
27
- return Array.from(compactLine(value)).slice(0, 80).join("");
28
- }
29
-
30
- /** The `required` names of an object schema, or none when it declares none. */
31
- export function requiredProperties(schema: unknown) {
32
- return isRecord(schema) && Array.isArray(schema.required)
33
- ? schema.required.filter((name): name is string => typeof name === "string")
34
- : [];
35
- }
36
-
37
- /** Property names in the schema's `x-order` first, then the rest as declared. */
38
- export function orderedPropertyNames(schema: Record<string, unknown>) {
39
- const properties = isRecord(schema.properties) ? schema.properties : {};
40
- const order = Array.isArray(schema["x-order"])
41
- ? schema["x-order"].filter(
42
- (name): name is string => typeof name === "string" && name in properties,
43
- )
44
- : [];
45
- return [...order, ...Object.keys(properties).filter((name) => !order.includes(name))];
46
- }
47
-
48
- const SCALAR_TYPES = new Set(["boolean", "number", "string"]);
49
-
50
- /**
51
- * Follows a local `$ref` (`#`, `#/$defs/x`, or `#/definitions/x`) to its
52
- * definition in the root document, keeping the keywords beside the ref (a
53
- * description, say), so a shared shape a converter hoisted (a tool's schema
54
- * rows) renders as its fields rather than `unknown`. A described ref comes out
55
- * of zod as a one-entry `allOf`, which reads the same. A recursive shape ends
56
- * where `MAX_DEPTH` ends it.
57
- */
58
- function resolver(root: unknown) {
59
- function definition(pointer: string) {
60
- if (pointer === "#") return root;
61
- const parts = pointer.split("/");
62
- const [, section, name] = parts;
63
- const definitions =
64
- isRecord(root) && (section === "$defs" || section === "definitions")
65
- ? root[section]
66
- : undefined;
67
- return parts.length === 3 && isRecord(definitions) && name !== undefined
68
- ? definitions[name]
69
- : undefined;
70
- }
71
- return function resolve(schema: unknown): unknown {
72
- if (!isRecord(schema)) return schema;
73
- const { $ref, allOf, ...rest } = schema;
74
- if (typeof $ref === "string") {
75
- const target = definition($ref);
76
- return isRecord(target) ? { ...target, ...rest } : target;
77
- }
78
- if (Array.isArray(allOf) && allOf.length === 1) {
79
- const target = resolve(allOf[0]);
80
- return isRecord(target) ? { ...target, ...rest } : target;
81
- }
82
- return schema;
83
- };
84
- }
85
-
86
- type Resolve = ReturnType<typeof resolver>;
87
-
88
- function literalUnion(values: unknown[]) {
89
- return values
90
- .filter((value) => value === null || SCALAR_TYPES.has(typeof value))
91
- .map((value) => JSON.stringify(value))
92
- .join(" | ");
93
- }
94
-
95
- /** An object's fields one per line, each typed, marked optional, and commented from its description. */
96
- function inputObject(
97
- schema: Record<string, unknown>,
98
- resolve: Resolve,
99
- depth: number,
100
- indent: number,
101
- ) {
102
- if (depth >= MAX_DEPTH) return "object";
103
- const properties = isRecord(schema.properties) ? schema.properties : {};
104
- const required = new Set(requiredProperties(schema));
105
- const fields = orderedPropertyNames(schema).map((name) => {
106
- const child = resolve(properties[name]);
107
- const comment = isRecord(child) ? fieldComment(child.description) : "";
108
- return `${" ".repeat(indent + 2)}${propertyName(name)}${required.has(name) ? "" : "?"}: ${inputType(child, resolve, depth + 1, indent + 2)},${comment ? ` // ${comment}` : ""}`;
109
- });
110
- return fields.length > 0 ? `{\n${fields.join("\n")}\n${" ".repeat(indent)}}` : "{}";
111
- }
112
-
113
- function inputType(schema: unknown, resolve: Resolve, depth = 0, indent = 0): string {
114
- schema = resolve(schema);
115
- if (!isRecord(schema)) return "unknown";
116
- if ("const" in schema && SCALAR_TYPES.has(typeof schema.const)) {
117
- return JSON.stringify(schema.const);
118
- }
119
- if (Array.isArray(schema.enum) && schema.enum.length > 0) return literalUnion(schema.enum);
120
- const variants = variantsOf(schema);
121
- if (variants) {
122
- const rendered = variants.map((variant) => inputType(variant, resolve, depth, indent));
123
- return [...new Set(rendered)].join(" | ");
124
- }
125
- const type = primaryType(schema.type);
126
- const nullable = allowsNull(schema.type) ? " | null" : "";
127
- if (type === "null") return "null";
128
- if (type === "array") {
129
- const item = inputType(schema.items, resolve, depth + 1, indent);
130
- // A union item type binds looser than `[]`, so it is parenthesized.
131
- return `${item.includes(" | ") ? `(${item})` : item}[]${nullable}`;
132
- }
133
- if (type === "object" || isRecord(schema.properties)) {
134
- return inputObject(schema, resolve, depth, indent) + nullable;
135
- }
136
- if (type === "integer") return `number${nullable}`;
137
- return SCALAR_TYPES.has(String(type)) ? `${String(type)}${nullable}` : "unknown";
138
- }
139
-
140
- /** The non-null member of a JSON Schema `type`, so `["string", "null"]` reads as `string`. */
141
- function primaryType(type: unknown) {
142
- if (!Array.isArray(type)) return type;
143
- const members: unknown[] = type;
144
- return members.find((member) => member !== "null");
145
- }
146
-
147
- function allowsNull(type: unknown) {
148
- if (!Array.isArray(type)) return false;
149
- const members: unknown[] = type;
150
- return members.includes("null");
151
- }
152
-
153
- /** The branches of a union schema, under whichever keyword carries them. */
154
- function variantsOf(schema: Record<string, unknown>) {
155
- const variants = [schema.oneOf, schema.anyOf].find(Array.isArray);
156
- return variants && variants.length > 0 ? variants : undefined;
157
- }
158
-
159
- function isObjectSchema(schema: unknown): schema is Record<string, unknown> {
160
- return (
161
- isRecord(schema) && (primaryType(schema.type) === "object" || isRecord(schema.properties))
162
- );
163
- }
164
-
165
- /**
166
- * A response schema as one shape: a union's object branches merged into the
167
- * fields any of them may carry, a `null` branch dropped. A script reads a
168
- * field off whichever branch came back, so the names are what matter.
169
- */
170
- function responseUnion(schema: unknown, resolve: Resolve): unknown {
171
- const resolved = resolve(schema);
172
- if (!isRecord(resolved)) return resolved;
173
- const variants = variantsOf(resolved);
174
- if (!variants) return resolved;
175
- const branches = variants
176
- .map((variant) => responseUnion(variant, resolve))
177
- .filter(
178
- (variant): variant is Record<string, unknown> =>
179
- isRecord(variant) && variant.type !== "null",
180
- );
181
- const objects = branches.filter(isObjectSchema);
182
- if (objects.length === 0) return branches[0];
183
- const properties: Record<string, unknown> = {};
184
- for (const object of objects) {
185
- if (isRecord(object.properties)) Object.assign(properties, object.properties);
186
- }
187
- return { type: "object", properties };
188
- }
189
-
190
- function responseObject(schema: Record<string, unknown>, resolve: Resolve, depth = 0): string {
191
- const properties = isRecord(schema.properties) ? schema.properties : {};
192
- const fields = orderedPropertyNames(schema).map((name) => {
193
- const child = responseUnion(properties[name], resolve);
194
- if (!isRecord(child) || depth >= MAX_DEPTH) return propertyName(name);
195
- if (primaryType(child.type) === "array") {
196
- const items = responseUnion(child.items, resolve);
197
- return isObjectSchema(items)
198
- ? `${propertyName(name)}[].${responseObject(items, resolve, depth + 1)}`
199
- : propertyName(name);
200
- }
201
- return isObjectSchema(child)
202
- ? `${propertyName(name)}.${responseObject(child, resolve, depth + 1)}`
203
- : propertyName(name);
204
- });
205
- return fields.length > 0 ? `{ ${fields.join(", ")} }` : "{}";
206
- }
207
-
208
- /**
209
- * The names a result carries, as a script would read them: `{ id, items[].{ name } }`.
210
- * A schema that describes no fields is `{}`, which is also what a tool that
211
- * declares no response gets; a caller that knows nothing passes `undefined`
212
- * and gets `unknown`, so the model does not mistake silence for emptiness.
213
- */
214
- export function responseShape(schema: unknown) {
215
- if (schema === undefined) return "unknown";
216
- const resolve = resolver(schema);
217
- const root = responseUnion(schema, resolve);
218
- if (!isRecord(root)) return "{}";
219
- if (primaryType(root.type) === "array") {
220
- const items = responseUnion(root.items, resolve);
221
- return isObjectSchema(items) ? `[]${responseObject(items, resolve)}` : "[]";
222
- }
223
- return responseObject(root, resolve);
224
- }
225
-
226
- /** One line, no comment terminator, and always a sentence, so it can sit in the doc comment. */
227
- function summarize(description: string) {
228
- const text = compactLine(description).replaceAll("*/", "* /");
229
- if (!text) return "";
230
- return /[.!?]$/.test(text) ? text : `${text}.`;
231
- }
232
-
233
- export type ToolSignatureInput = {
234
- /** As the script calls it: a name dot access cannot reach renders as the index call `tools["a-b"]`. */
235
- name: string;
236
- description: string;
237
- /** JSON Schema of the input object as the model should see it. */
238
- inputSchema: unknown;
239
- /** Already rendered (a shared-shape ref like `$R1`), or the JSON Schema to render, or undefined for "unknown". */
240
- returns: string | { schema: unknown };
241
- /** Appended to the summary; the caller knows what else the model should be told (idempotency, embedded flows). */
242
- notes?: ReadonlyArray<string>;
243
- };
244
-
245
- /** `/** summary *\/\nname(input: {...}): { fields }`. */
246
- export function renderToolSignature(tool: ToolSignatureInput) {
247
- const summary = [summarize(tool.description), ...(tool.notes ?? [])].filter(Boolean).join(" ");
248
- const callee = IDENTIFIER_PATTERN.test(tool.name)
249
- ? tool.name
250
- : `tools[${JSON.stringify(tool.name)}]`;
251
- const returns =
252
- typeof tool.returns === "string" ? tool.returns : responseShape(tool.returns.schema);
253
- const input = inputType(tool.inputSchema, resolver(tool.inputSchema));
254
- return `${summary ? `/** ${summary} */\n` : ""}${callee}(input: ${input}): ${returns}`;
255
- }