@instruktlabs/kiln 1.0.0 → 1.2.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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +60 -3
- package/CONTRIBUTING.md +16 -7
- package/README.md +95 -381
- package/dist/agent-run.mjs +220 -150
- package/dist/build.json +40 -28
- package/dist/cli.mjs +242 -163
- package/dist/mcp-engine.mjs +412 -154
- package/dist/mcp-server.mjs +1 -1
- package/docs/architecture.md +6 -3
- package/docs/chatgpt.md +20 -5
- package/docs/examples.md +40 -170
- package/docs/install.md +100 -35
- package/docs/migration.md +20 -0
- package/docs/sdk.md +49 -5
- package/docs/tools-operations.md +1529 -0
- package/lib/asset-widget-uri.d.ts +9 -0
- package/lib/asset-widget-uri.js +9 -0
- package/lib/cli-render-mode.js +10 -3
- package/lib/discovery/index.d.ts +1 -1
- package/lib/discovery/index.js +2 -2
- package/lib/discovery/portable.d.ts +9 -0
- package/lib/discovery/portable.js +6 -0
- package/lib/discovery/service.d.ts +3 -1
- package/lib/discovery/service.js +4 -2
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/isolation.js +19 -5
- package/lib/evaluator/probe-invariants.d.ts +11 -0
- package/lib/evaluator/probe-invariants.js +36 -0
- package/lib/evaluator/probe-worker.js +16 -4
- package/lib/loopback.d.ts +6 -0
- package/lib/loopback.js +6 -0
- package/lib/mcp-core.d.ts +210 -0
- package/lib/mcp-core.js +279 -0
- package/lib/mcp-engine.d.ts +59 -0
- package/lib/mcp-engine.js +289 -0
- package/lib/mcp-manifest.d.ts +15 -0
- package/lib/mcp-manifest.js +65 -0
- package/lib/mcp.d.ts +5 -0
- package/lib/mcp.js +2 -0
- package/lib/render-service-host.js +1 -1
- package/lib/tools/discovery.d.ts +1 -1
- package/lib/tools/discovery.js +11 -9
- package/lib/tools/materials.d.ts +1 -1
- package/lib/tools/materials.js +44 -14
- package/lib/tools/operation-contract.d.ts +8 -0
- package/lib/tools/operation-contract.js +37 -0
- package/lib/tools/operations.d.ts +3 -0
- package/lib/tools/operations.js +154 -0
- package/lib/tools/registry.d.ts +6 -2
- package/lib/tools/registry.js +71 -23
- package/package.json +23 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/icon.png +0 -0
- package/plugins/kiln-engine/.claude-plugin/plugin.json +9 -3
- package/plugins/kiln-engine/PRIVACY.md +100 -0
- package/plugins/kiln-engine/README.md +103 -18
- package/plugins/kiln-engine/{scripts/setup-workspace.mjs → bin/kiln-setup-workspace.mjs} +37 -9
- package/plugins/kiln-engine/package-provenance.json +15 -8
- package/plugins/kiln-engine/plugin.json +10 -4
- package/plugins/kiln-engine/runtime.json +10 -1
- package/plugins/kiln-engine/skills/kiln-setup-workspace/SKILL.md +17 -18
- package/plugins/kiln-engine/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
- package/plugins/kiln-engine/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
- package/scripts/create-workspace.mjs +71 -13
- package/scripts/workspace-config.mjs +239 -0
- package/scripts/workspace-project.mjs +384 -0
- package/scripts/workspace-transaction.mjs +504 -0
- package/skills/kiln-setup-workspace/SKILL.md +17 -18
- package/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
- package/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
- package/src/cli-render-mode.ts +9 -9
- package/src/cli.ts +3 -3
- package/src/discovery/index.ts +2 -0
- package/src/discovery/portable.ts +9 -0
- package/src/discovery/service.ts +3 -1
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/isolation.ts +27 -6
- package/src/evaluator/probe-invariants.ts +50 -0
- package/src/evaluator/probe-worker.ts +22 -4
- package/src/mcp-core.ts +3 -0
- package/src/mcp-engine.ts +11 -1
- package/src/mcp.ts +5 -0
- package/src/render-service-host.ts +1 -1
- package/src/tools/discovery.ts +20 -10
- package/src/tools/materials.ts +59 -15
- package/src/tools/operation-contract.ts +51 -0
- package/src/tools/operations.ts +177 -0
- package/src/tools/registry.ts +100 -20
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { localAssetLibrary } from "./assets-node.js";
|
|
2
|
+
import { createLocalToolContext } from "./local-runtime.js";
|
|
3
|
+
import { KILN_RESOURCE_TEMPLATES, KILN_WIDGET_RESOURCE, } from "./mcp-core.js";
|
|
4
|
+
import { createKilnProgramToolRegistry, } from "./tools/registry.js";
|
|
5
|
+
const JSON_SCHEMA_TARGET = 'draft-2020-12';
|
|
6
|
+
function standardJsonSchema(schema, io) {
|
|
7
|
+
const carrier = schema;
|
|
8
|
+
const converter = carrier['~standard'].jsonSchema;
|
|
9
|
+
if (!converter)
|
|
10
|
+
throw new Error('The registry schema does not implement Standard JSON Schema (zod >= 4.2).');
|
|
11
|
+
const { $schema: _dialect, ...rest } = converter[io]({ target: JSON_SCHEMA_TARGET });
|
|
12
|
+
return rest;
|
|
13
|
+
}
|
|
14
|
+
/** The input schema as advertised: an object root first, the rest as converted. */
|
|
15
|
+
export function toolInputJsonSchema(def) {
|
|
16
|
+
const converted = standardJsonSchema(def.inputSchema, 'input');
|
|
17
|
+
if (converted['type'] !== undefined && converted['type'] !== 'object')
|
|
18
|
+
throw new Error(`${def.name}: a tool input schema must describe an object.`);
|
|
19
|
+
return { type: 'object', ...converted };
|
|
20
|
+
}
|
|
21
|
+
/** One registry definition as one `tools/list` entry, keys in the order the wire has always carried them. */
|
|
22
|
+
export function toolManifestEntry(def) {
|
|
23
|
+
return {
|
|
24
|
+
name: def.name,
|
|
25
|
+
description: def.description,
|
|
26
|
+
inputSchema: toolInputJsonSchema(def),
|
|
27
|
+
...(def.annotations ? { annotations: def.annotations } : {}),
|
|
28
|
+
...(def.ui
|
|
29
|
+
? {
|
|
30
|
+
_meta: {
|
|
31
|
+
ui: { resourceUri: def.ui.resourceUri, visibility: ['model'] },
|
|
32
|
+
'openai/outputTemplate': def.ui.resourceUri,
|
|
33
|
+
'ui/resourceUri': def.ui.resourceUri,
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
: {}),
|
|
37
|
+
...(def.outputSchema
|
|
38
|
+
? { outputSchema: { type: 'object', ...standardJsonSchema(def.outputSchema, 'output') } }
|
|
39
|
+
: {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/** Which templates a context can serve: asset files need a library, project packages a bundle reader. */
|
|
43
|
+
export function resourceTemplatesFor(context) {
|
|
44
|
+
return [
|
|
45
|
+
...(context.assetLibrary ? [KILN_RESOURCE_TEMPLATES.assetFile] : []),
|
|
46
|
+
...(context.projectBundleReader ? [KILN_RESOURCE_TEMPLATES.projectPackage] : []),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
export function buildMcpManifest(defs, context) {
|
|
50
|
+
return {
|
|
51
|
+
kind: 'kiln.mcp-manifest.v1',
|
|
52
|
+
tools: defs.map(toolManifestEntry),
|
|
53
|
+
resources: [KILN_WIDGET_RESOURCE],
|
|
54
|
+
resourceTemplates: resourceTemplatesFor(context),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The manifest the packaged server advertises: the full surface with both
|
|
59
|
+
* templates, built from a context shaped like the packaged host's but with no
|
|
60
|
+
* environment of its own, so the file is the same on every machine.
|
|
61
|
+
*/
|
|
62
|
+
export function packagedMcpManifest() {
|
|
63
|
+
const context = createLocalToolContext({ assetLibrary: localAssetLibrary() }, {});
|
|
64
|
+
return buildMcpManifest(createKilnProgramToolRegistry(context), context);
|
|
65
|
+
}
|
package/lib/mcp.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Node embedding API. The stdio executable retains its separate lazy entry. */
|
|
2
|
+
export { createKilnMcpServer, createKilnToolHost, kilnMcpToolDefs } from "./mcp-engine.js";
|
|
3
|
+
export type { KilnMcpCompatibilityOptions, KilnToolResult } from "./mcp-engine.js";
|
|
4
|
+
export type { KilnMcpHost, KilnContentBlock, KilnResourceContents } from "./mcp-core.js";
|
|
5
|
+
export type { KilnToolContext, KilnToolDef } from "./tools/registry.js";
|
package/lib/mcp.js
ADDED
|
@@ -224,7 +224,7 @@ export async function startLocalRenderService(dir = renderServiceDir(), environm
|
|
|
224
224
|
// that listener a bounded health warmup, and join only after full verification.
|
|
225
225
|
// A failed spawn has no such race; absent/unknown never count as success.
|
|
226
226
|
if (spawnError || (exitedAt !== undefined && Date.now() - exitedAt >= 1500))
|
|
227
|
-
throw new Error(`render service exited during startup${spawnError ? `: ${spawnError.message}` : ''}${stderr.trim() ? `: ${stderr.trim()
|
|
227
|
+
throw new Error(`render service exited during startup${spawnError ? `: ${spawnError.message}` : ''}${stderr.trim() ? `: ${stderr.trim()}` : windowsLaunch ? '; launch the renderer manually to inspect native-driver errors' : ''}`);
|
|
228
228
|
await new Promise((done) => setTimeout(done, 250));
|
|
229
229
|
}
|
|
230
230
|
throw new Error(`render service health is unknown after ${budget}ms; inspect with \`kiln service reprobe\``);
|
package/lib/tools/discovery.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { KilnToolContext, KilnToolDef } from "./registry.js";
|
|
2
2
|
/** Shared native tool definition; all transports use the same Discovery service. */
|
|
3
|
-
export declare function createKilnDiscoveryDef(context: KilnToolContext): KilnToolDef;
|
|
3
|
+
export declare function createKilnDiscoveryDef(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef;
|
package/lib/tools/discovery.js
CHANGED
|
@@ -8,8 +8,8 @@ import { geometryExportAttributes } from "../geometry-export.js";
|
|
|
8
8
|
import { resolveGltfExporter } from "../community-exporter.js";
|
|
9
9
|
import { approvedTextureCatalogV1 } from "../material-resources.js";
|
|
10
10
|
/** Shared native tool definition; all transports use the same Discovery service. */
|
|
11
|
-
export function createKilnDiscoveryDef(context) {
|
|
12
|
-
const run = createDiscovery(() => currentCapabilities(context), async () => projectNotes(context));
|
|
11
|
+
export function createKilnDiscoveryDef(context, presentation = 'grouped') {
|
|
12
|
+
const run = createDiscovery(() => currentCapabilities(context, presentation), async () => projectNotes(context), presentation === 'operations' ? 'kiln_capabilities({})' : 'capabilities:true');
|
|
13
13
|
return {
|
|
14
14
|
name: 'kiln_discover',
|
|
15
15
|
description: 'Discover Kiln operations, assemblies, recipes, tool-input shapes and current host capabilities. Omit arguments for a compact overview. Search with ordinary modeling language using query; refine with family, kind or tags. Fetch complete contracts/examples with ids (up to six exact IDs, executable names or shape: ids). Overview/search pages default to six summaries. Recipes guide construction without restricting the asset. Search runs locally without models or network calls.',
|
|
@@ -39,7 +39,7 @@ function projectNotes(context) {
|
|
|
39
39
|
]
|
|
40
40
|
: [];
|
|
41
41
|
}
|
|
42
|
-
async function currentCapabilities(context) {
|
|
42
|
+
async function currentCapabilities(context, presentation) {
|
|
43
43
|
const externalEvaluator = Boolean(context.evaluatorPort) || context.evaluatorProfile === 'evaluator-required';
|
|
44
44
|
const approvedTextures = context.approvedTextureResources
|
|
45
45
|
? context.approvedTextureResources()
|
|
@@ -91,11 +91,13 @@ async function currentCapabilities(context) {
|
|
|
91
91
|
// you -- so the workspace guide's "do not substitute it silently" had
|
|
92
92
|
// nothing to check against. Compare with `runtime` in .kiln/workspace.json.
|
|
93
93
|
engine: engineIdentity(),
|
|
94
|
-
project:
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
94
|
+
project: presentation === 'operations' && !context.projectStore
|
|
95
|
+
? { available: false }
|
|
96
|
+
: {
|
|
97
|
+
configured: configuredProject(context),
|
|
98
|
+
select: 'projectId per call; omitted uses the configured project, null selects standalone work',
|
|
99
|
+
read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
|
|
100
|
+
},
|
|
99
101
|
renderer,
|
|
100
102
|
execution: context.localExecution ??
|
|
101
103
|
(context.evaluatorPort
|
|
@@ -115,7 +117,7 @@ async function currentCapabilities(context) {
|
|
|
115
117
|
available: Boolean(context.assetLibrary),
|
|
116
118
|
collections: context.assetLibrary?.collections() ?? [],
|
|
117
119
|
save: 'kiln_save persists exact GLB, source and provenance; draft renders do not populate collections',
|
|
118
|
-
resume: 'kiln_assets action=restore imports a saved revision into the current program store
|
|
120
|
+
resume: `${presentation === 'operations' ? 'kiln_assets_restore' : 'kiln_assets action=restore'} imports a saved revision into the current program store`,
|
|
119
121
|
downloads: 'kiln_export returns GLB/source/ZIP resource links; client presentation varies',
|
|
120
122
|
viewer: 'kiln_present opens a saved revision in supporting chat clients with 3D viewing and downloads; kiln view opens a local collection or standalone GLB/ZIP',
|
|
121
123
|
},
|
package/lib/tools/materials.d.ts
CHANGED
|
@@ -26,4 +26,4 @@ export declare const materialToolInput: z.ZodObject<{
|
|
|
26
26
|
draft: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
27
27
|
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
28
28
|
}, z.core.$strict>;
|
|
29
|
-
export declare function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef;
|
|
29
|
+
export declare function createKilnMaterialDef(library: MaterialLibrary, presentation?: 'grouped' | 'operations'): KilnToolDef;
|
package/lib/tools/materials.js
CHANGED
|
@@ -11,13 +11,39 @@ const MATERIAL_ACTIONS = [
|
|
|
11
11
|
'create-procedural',
|
|
12
12
|
'import',
|
|
13
13
|
];
|
|
14
|
-
const
|
|
15
|
-
presets: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
14
|
+
const MATERIAL_CONTRACT = {
|
|
15
|
+
presets: {
|
|
16
|
+
fields: ['tag'],
|
|
17
|
+
required: [],
|
|
18
|
+
description: 'List shipped material recipes, optionally filtered by tag.',
|
|
19
|
+
},
|
|
20
|
+
'create-preset': {
|
|
21
|
+
fields: ['presetId', 'seed', 'creator', 'license', 'size', 'materialId'],
|
|
22
|
+
required: ['presetId', 'seed', 'creator', 'license'],
|
|
23
|
+
description: 'Create an immutable material from a shipped preset with explicit seed, creator and license.',
|
|
24
|
+
},
|
|
25
|
+
list: {
|
|
26
|
+
fields: ['tag'],
|
|
27
|
+
required: [],
|
|
28
|
+
description: 'List saved immutable material revisions, optionally filtered by tag.',
|
|
29
|
+
},
|
|
30
|
+
get: {
|
|
31
|
+
fields: ['materialId', 'revisionId'],
|
|
32
|
+
required: ['materialId'],
|
|
33
|
+
description: 'Read a material revision, provenance and portable specification. Omit revisionId only when exactly one revision exists.',
|
|
34
|
+
},
|
|
35
|
+
'create-procedural': {
|
|
36
|
+
fields: ['draft'],
|
|
37
|
+
required: ['draft'],
|
|
38
|
+
shapes: { draft: 'shape:material-draft' },
|
|
39
|
+
description: 'Create an immutable material from a bounded procedural draft. Does not fetch URLs or execute source.',
|
|
40
|
+
},
|
|
41
|
+
import: {
|
|
42
|
+
fields: ['payload'],
|
|
43
|
+
required: ['payload'],
|
|
44
|
+
shapes: { payload: 'shape:material-import' },
|
|
45
|
+
description: 'Import complete normalized material records and embedded PNGs. Does not fetch URLs or execute source.',
|
|
46
|
+
},
|
|
21
47
|
};
|
|
22
48
|
const record = (description) => z.record(z.string(), z.unknown()).optional().describe(description);
|
|
23
49
|
const tag = z.string().min(1).max(80).optional().describe('presets, list: keep one tag.');
|
|
@@ -49,14 +75,14 @@ const materialResult = (material) => ({
|
|
|
49
75
|
material,
|
|
50
76
|
portableSpec: materialLibraryPortableSpec(material),
|
|
51
77
|
});
|
|
52
|
-
const unknownMaterial = (materialId, revisionId) => new Error(`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}.
|
|
78
|
+
const unknownMaterial = (materialId, revisionId, listCall) => new Error(`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. ${listCall} lists the materials in this workspace with their revision IDs.`);
|
|
53
79
|
/**
|
|
54
80
|
* The revision a `get` without `revisionId` means: the material's only one.
|
|
55
81
|
* Revisions are content hashes with no order, so several are named for the
|
|
56
82
|
* caller to choose (two live sessions of 1 October 2026 called get with the
|
|
57
83
|
* id alone and were refused).
|
|
58
84
|
*/
|
|
59
|
-
async function onlyRevision(library, materialId) {
|
|
85
|
+
async function onlyRevision(library, materialId, listCall) {
|
|
60
86
|
const revisions = (await library.list())
|
|
61
87
|
.filter((material) => material.materialId === materialId)
|
|
62
88
|
.map((material) => material.revisionId)
|
|
@@ -64,7 +90,7 @@ async function onlyRevision(library, materialId) {
|
|
|
64
90
|
if (revisions.length === 1)
|
|
65
91
|
return revisions[0];
|
|
66
92
|
if (revisions.length === 0)
|
|
67
|
-
throw unknownMaterial(materialId);
|
|
93
|
+
throw unknownMaterial(materialId, undefined, listCall);
|
|
68
94
|
throw new Error(`Material ${materialId} has ${revisions.length} revisions: ${revisions.join(', ')}. Pass revisionId to get one.`);
|
|
69
95
|
}
|
|
70
96
|
/**
|
|
@@ -83,11 +109,15 @@ function aliasMaterialId(raw) {
|
|
|
83
109
|
throw new Error(`kiln_material: resourceId (${String(resourceId)}) and materialId (${String(rest.materialId)}) name different materials; pass materialId alone.`);
|
|
84
110
|
return { ...rest, materialId: resourceId };
|
|
85
111
|
}
|
|
86
|
-
export function createKilnMaterialDef(library) {
|
|
112
|
+
export function createKilnMaterialDef(library, presentation = 'grouped') {
|
|
113
|
+
const listCall = presentation === 'operations'
|
|
114
|
+
? 'kiln_material_search({ scope: "saved" })'
|
|
115
|
+
: "kiln_material { action: 'list' }";
|
|
87
116
|
return {
|
|
88
117
|
name: 'kiln_material',
|
|
89
118
|
description: 'Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.',
|
|
90
119
|
inputSchema: materialToolInput,
|
|
120
|
+
actionContract: MATERIAL_CONTRACT,
|
|
91
121
|
annotations: {
|
|
92
122
|
readOnlyHint: false,
|
|
93
123
|
destructiveHint: false,
|
|
@@ -97,7 +127,7 @@ export function createKilnMaterialDef(library) {
|
|
|
97
127
|
async run(raw) {
|
|
98
128
|
assertMaterialJson(raw);
|
|
99
129
|
const input = materialToolInput.parse(aliasMaterialId(raw));
|
|
100
|
-
requireActionFields('kiln_material', input.action, input,
|
|
130
|
+
requireActionFields('kiln_material', input.action, input, MATERIAL_CONTRACT[input.action]);
|
|
101
131
|
switch (input.action) {
|
|
102
132
|
case 'presets':
|
|
103
133
|
return {
|
|
@@ -135,14 +165,14 @@ export function createKilnMaterialDef(library) {
|
|
|
135
165
|
};
|
|
136
166
|
}
|
|
137
167
|
case 'get': {
|
|
138
|
-
const revisionId = input.revisionId ?? (await onlyRevision(library, input.materialId));
|
|
168
|
+
const revisionId = input.revisionId ?? (await onlyRevision(library, input.materialId, listCall));
|
|
139
169
|
try {
|
|
140
170
|
return materialResult((await library.read(input.materialId, revisionId)).manifest);
|
|
141
171
|
}
|
|
142
172
|
catch (error) {
|
|
143
173
|
// Rule 9: an unknown id names the call that lists the known ones.
|
|
144
174
|
if (error.code === 'ENOENT')
|
|
145
|
-
throw unknownMaterial(input.materialId, revisionId);
|
|
175
|
+
throw unknownMaterial(input.materialId, revisionId, listCall);
|
|
146
176
|
throw error;
|
|
147
177
|
}
|
|
148
178
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ActionRequirements } from "./actions.js";
|
|
3
|
+
/** Domain action metadata shared by grouped execution and named presentations. */
|
|
4
|
+
export interface KilnActionContract extends ActionRequirements {
|
|
5
|
+
readonly fields: readonly string[];
|
|
6
|
+
readonly description: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function projectActionSchemas<A extends string>(name: string, input: z.ZodObject, actions: Readonly<Record<A, KilnActionContract>>): Record<A, z.ZodObject>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function projectActionSchemas(name, input, actions) {
|
|
3
|
+
if (input.def.checks?.length)
|
|
4
|
+
throw new Error(`${name}: cross-field constraints require an explicit operation schema`);
|
|
5
|
+
const json = z.toJSONSchema(input, { io: 'input' });
|
|
6
|
+
const selector = json.properties?.action;
|
|
7
|
+
const expected = Object.keys(actions).sort();
|
|
8
|
+
if (!Array.isArray(selector?.enum) ||
|
|
9
|
+
JSON.stringify([...selector.enum].sort()) !== JSON.stringify(expected))
|
|
10
|
+
throw new Error(`${name}: unclassified actions`);
|
|
11
|
+
const classified = new Set([
|
|
12
|
+
'action',
|
|
13
|
+
...Object.values(actions).flatMap((contract) => [...contract.fields]),
|
|
14
|
+
]);
|
|
15
|
+
const unclassified = Object.keys(input.shape).filter((field) => !classified.has(field));
|
|
16
|
+
if (unclassified.length)
|
|
17
|
+
throw new Error(`${name}: Unclassified fields: ${unclassified.join(', ')}`);
|
|
18
|
+
const schemas = [];
|
|
19
|
+
for (const [action, contract] of Object.entries(actions)) {
|
|
20
|
+
for (const field of contract.required)
|
|
21
|
+
if (!contract.fields.includes(field))
|
|
22
|
+
throw new Error(`${name}.${action}: unmapped required ${field}`);
|
|
23
|
+
for (const field of json.required ?? [])
|
|
24
|
+
if (field !== 'action' && !contract.fields.includes(field))
|
|
25
|
+
throw new Error(`${name}.${action}: would omit required ${field}`);
|
|
26
|
+
const shape = {};
|
|
27
|
+
for (const field of contract.fields) {
|
|
28
|
+
if (field === 'action' || !Object.hasOwn(input.shape, field))
|
|
29
|
+
throw new Error(`${name}.${action}: missing or reserved field ${field}`);
|
|
30
|
+
classified.add(field);
|
|
31
|
+
const source = input.shape[field];
|
|
32
|
+
shape[field] = contract.required.includes(field) ? source.nonoptional() : source;
|
|
33
|
+
}
|
|
34
|
+
schemas.push([action, z.strictObject(shape)]);
|
|
35
|
+
}
|
|
36
|
+
return Object.fromEntries(schemas);
|
|
37
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { projectActionSchemas } from "./operation-contract.js";
|
|
3
|
+
/** Adapt domain operations in the engine, never in a transport-specific skin. */
|
|
4
|
+
export function projectOperationDefs(defs, context) {
|
|
5
|
+
return defs.flatMap((def) => {
|
|
6
|
+
if (!context.assetLibrary &&
|
|
7
|
+
['kiln_save', 'kiln_assets', 'kiln_present', 'kiln_export', 'kiln_import'].includes(def.name))
|
|
8
|
+
return [];
|
|
9
|
+
if (def.name === 'kiln_renderer')
|
|
10
|
+
return rendererOperations(def, context);
|
|
11
|
+
if (def.name === 'kiln_discover')
|
|
12
|
+
return discoveryOperations(def);
|
|
13
|
+
if (def.name === 'kiln_material')
|
|
14
|
+
return materialOperations(def);
|
|
15
|
+
if (def.name === 'kiln_assets')
|
|
16
|
+
return assetOperations(def, context);
|
|
17
|
+
return [def];
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function actionTools(def) {
|
|
21
|
+
if (!(def.inputSchema instanceof z.ZodObject) || !def.actionContract)
|
|
22
|
+
throw new Error(`${def.name}: missing shared action contract`);
|
|
23
|
+
const schemas = projectActionSchemas(def.name, def.inputSchema, def.actionContract);
|
|
24
|
+
return Object.fromEntries(Object.entries(def.actionContract).map(([action, contract]) => [
|
|
25
|
+
action,
|
|
26
|
+
{
|
|
27
|
+
...def,
|
|
28
|
+
actionContract: undefined,
|
|
29
|
+
name: `${def.name}_${action.replaceAll('-', '_')}`,
|
|
30
|
+
description: contract.description,
|
|
31
|
+
inputSchema: schemas[action],
|
|
32
|
+
run: async (raw) => def.run({ ...schemas[action].parse(raw), action }),
|
|
33
|
+
},
|
|
34
|
+
]));
|
|
35
|
+
}
|
|
36
|
+
function assetOperations(def, context) {
|
|
37
|
+
const tools = actionTools(def);
|
|
38
|
+
const list = tools.list;
|
|
39
|
+
const listSchema = list.inputSchema;
|
|
40
|
+
// Remove only the legacy default: omission means all collections in this operation.
|
|
41
|
+
const collection = listSchema.shape.collection
|
|
42
|
+
.unwrap()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe('Collection ID from kiln_assets_collections; omitted searches all configured collections.');
|
|
45
|
+
const inputSchema = listSchema.extend({ collection });
|
|
46
|
+
const search = {
|
|
47
|
+
...list,
|
|
48
|
+
name: 'kiln_assets_search',
|
|
49
|
+
description: 'Search saved assets with pagination. Omit collection for all configured collections, or select one explicitly. Results identify exact collection, asset and revision; partial read failures are reported.',
|
|
50
|
+
inputSchema,
|
|
51
|
+
run: async (raw) => {
|
|
52
|
+
const input = inputSchema.parse(raw);
|
|
53
|
+
const { collection: _collection, ...filters } = input;
|
|
54
|
+
if (input.collection === undefined)
|
|
55
|
+
return tools.catalog.run(filters);
|
|
56
|
+
if (!context.assetLibrary?.collections().some((item) => item.id === input.collection))
|
|
57
|
+
throw new Error('Unknown collection. Use kiln_assets_collections to list available destinations.');
|
|
58
|
+
const result = (await list.run(input));
|
|
59
|
+
return {
|
|
60
|
+
errors: [],
|
|
61
|
+
total: result.total,
|
|
62
|
+
nextOffset: result.nextOffset,
|
|
63
|
+
assets: result.assets.map((asset) => ({ ...asset, collection: input.collection })),
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
return [tools.collections, search, tools.get, tools.restore];
|
|
68
|
+
}
|
|
69
|
+
function materialOperations(def) {
|
|
70
|
+
const tools = actionTools(def);
|
|
71
|
+
const presets = tools.presets;
|
|
72
|
+
const inputSchema = presets.inputSchema.extend({
|
|
73
|
+
scope: z.enum(['all', 'presets', 'saved']).default('all'),
|
|
74
|
+
});
|
|
75
|
+
const search = {
|
|
76
|
+
...presets,
|
|
77
|
+
name: 'kiln_material_search',
|
|
78
|
+
description: 'Browse shipped material recipes and saved material revisions, optionally filtered by exact tag. Recipe IDs and saved material IDs are distinct. Use scope to select presets, saved or both.',
|
|
79
|
+
inputSchema,
|
|
80
|
+
run: async (raw) => {
|
|
81
|
+
const { scope, ...input } = inputSchema.parse(raw);
|
|
82
|
+
const recipes = scope === 'saved'
|
|
83
|
+
? { presets: [] }
|
|
84
|
+
: (await presets.run(input));
|
|
85
|
+
const saved = scope === 'presets'
|
|
86
|
+
? { materials: [] }
|
|
87
|
+
: (await tools.list.run(input));
|
|
88
|
+
return {
|
|
89
|
+
ok: true,
|
|
90
|
+
presets: recipes.presets.map((item) => ({ ...item, kind: 'preset' })),
|
|
91
|
+
materials: saved.materials.map((item) => ({ ...item, kind: 'saved' })),
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const preset = tools['create-preset'];
|
|
96
|
+
const procedural = tools['create-procedural'];
|
|
97
|
+
const createInput = z.strictObject({
|
|
98
|
+
definition: z.discriminatedUnion('kind', [
|
|
99
|
+
preset.inputSchema.extend({ kind: z.literal('preset') }),
|
|
100
|
+
procedural.inputSchema.extend({ kind: z.literal('procedural') }),
|
|
101
|
+
]),
|
|
102
|
+
});
|
|
103
|
+
const create = {
|
|
104
|
+
...preset,
|
|
105
|
+
name: 'kiln_material_create',
|
|
106
|
+
description: 'Create and retain one immutable material from a preset or procedural definition. Presets require explicit seed, creator and license. Pin the returned materialId/revisionId for rendering. Does not fetch URLs or execute source.',
|
|
107
|
+
inputSchema: createInput,
|
|
108
|
+
run: async (raw) => {
|
|
109
|
+
const { kind, ...input } = createInput.parse(raw).definition;
|
|
110
|
+
return (kind === 'preset' ? preset : procedural).run(input);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
return [search, tools.get, create, tools.import];
|
|
114
|
+
}
|
|
115
|
+
function rendererOperations(def, context) {
|
|
116
|
+
if (!context.renderCapabilities)
|
|
117
|
+
return [];
|
|
118
|
+
return ['status', ...(context.reprobeRenderer ? ['reprobe'] : [])].map((action) => {
|
|
119
|
+
const inputSchema = z.strictObject({});
|
|
120
|
+
return {
|
|
121
|
+
...def,
|
|
122
|
+
name: `kiln_renderer_${action}`,
|
|
123
|
+
inputSchema,
|
|
124
|
+
run: async (raw) => {
|
|
125
|
+
inputSchema.parse(raw);
|
|
126
|
+
return def.run({ action });
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function discoveryOperations(def) {
|
|
132
|
+
const original = def.inputSchema;
|
|
133
|
+
const { capabilities: _capabilities, ...fields } = original.shape;
|
|
134
|
+
const inputSchema = z.strictObject(fields);
|
|
135
|
+
const empty = z.strictObject({});
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
...def,
|
|
139
|
+
description: 'Discover geometry helpers, assemblies, recipes and complete input shapes. Search with modeling language or fetch up to six exact IDs. Use kiln_capabilities for current host facts.',
|
|
140
|
+
inputSchema,
|
|
141
|
+
run: async (raw) => def.run(inputSchema.parse(raw)),
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
...def,
|
|
145
|
+
name: 'kiln_capabilities',
|
|
146
|
+
description: 'Inspect declared host capabilities, execution limits and available storage. Observed rendering fidelity is returned by each render.',
|
|
147
|
+
inputSchema: empty,
|
|
148
|
+
run: async (raw) => {
|
|
149
|
+
empty.parse(raw);
|
|
150
|
+
return def.run({ capabilities: true });
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
];
|
|
154
|
+
}
|
package/lib/tools/registry.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { type BackdropId } from "../views/background.js";
|
|
|
19
19
|
import type { TextureUsage } from "../textures.js";
|
|
20
20
|
export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
|
|
21
21
|
export interface KilnToolDef {
|
|
22
|
+
/** Shared action ownership and requirements; never hand-maintained by transports. */
|
|
23
|
+
actionContract?: Readonly<Record<string, import("./operation-contract.js").KilnActionContract>>;
|
|
22
24
|
outputSchema?: z.ZodType;
|
|
23
25
|
/** Optional MCP App presentation; metadata is hidden from the language model. */
|
|
24
26
|
ui?: {
|
|
@@ -770,6 +772,8 @@ export declare function createKilnEditDef(context?: KilnToolContext): KilnToolDe
|
|
|
770
772
|
/** Neutral compatibility export. */
|
|
771
773
|
export declare const kilnEditDef: KilnToolDef;
|
|
772
774
|
export declare function createKilnProgramToolRegistry(suppliedContext?: KilnToolContext): KilnToolDef[];
|
|
775
|
+
/** Opt-in named domain operations; grouped local/native contracts remain unchanged. */
|
|
776
|
+
export declare function createKilnOperationToolRegistry(context?: KilnToolContext): KilnToolDef[];
|
|
773
777
|
/** Canonical native workflow: shared definitions plus one host-owned terminal. */
|
|
774
778
|
export declare function createKilnNativeToolRegistry(suppliedContext: KilnToolContext, completion: NativeCompletion): KilnToolDef[];
|
|
775
779
|
/**
|
|
@@ -784,9 +788,9 @@ export declare function newestRevision(target: {
|
|
|
784
788
|
parentRevision?: string | null;
|
|
785
789
|
createdAt: string;
|
|
786
790
|
}[]>;
|
|
787
|
-
}, collection: string, assetId: string): Promise<string>;
|
|
791
|
+
}, collection: string, assetId: string, listCall?: string): Promise<string>;
|
|
788
792
|
/** Build one current-policy revision for ordinary save or explicit migration. */
|
|
789
793
|
export declare function buildProgramAssetDraft(code: string, context: KilnToolContext, backdrop?: BackdropId): Promise<Pick<import("../assets.js").AssetDraft, 'code' | 'glb' | 'preview' | 'previewInfo' | 'build'>>;
|
|
790
794
|
/** All asset operation schemas live here, alongside the existing tool definitions. */
|
|
791
|
-
export declare function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[];
|
|
795
|
+
export declare function createKilnAssetDefs(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef[];
|
|
792
796
|
export {};
|