@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.
Files changed (92) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/CHANGELOG.md +60 -3
  5. package/CONTRIBUTING.md +16 -7
  6. package/README.md +95 -381
  7. package/dist/agent-run.mjs +220 -150
  8. package/dist/build.json +40 -28
  9. package/dist/cli.mjs +242 -163
  10. package/dist/mcp-engine.mjs +412 -154
  11. package/dist/mcp-server.mjs +1 -1
  12. package/docs/architecture.md +6 -3
  13. package/docs/chatgpt.md +20 -5
  14. package/docs/examples.md +40 -170
  15. package/docs/install.md +100 -35
  16. package/docs/migration.md +20 -0
  17. package/docs/sdk.md +49 -5
  18. package/docs/tools-operations.md +1529 -0
  19. package/lib/asset-widget-uri.d.ts +9 -0
  20. package/lib/asset-widget-uri.js +9 -0
  21. package/lib/cli-render-mode.js +10 -3
  22. package/lib/discovery/index.d.ts +1 -1
  23. package/lib/discovery/index.js +2 -2
  24. package/lib/discovery/portable.d.ts +9 -0
  25. package/lib/discovery/portable.js +6 -0
  26. package/lib/discovery/service.d.ts +3 -1
  27. package/lib/discovery/service.js +4 -2
  28. package/lib/engine-identity.d.ts +1 -1
  29. package/lib/engine-identity.js +1 -1
  30. package/lib/evaluator/isolation.js +19 -5
  31. package/lib/evaluator/probe-invariants.d.ts +11 -0
  32. package/lib/evaluator/probe-invariants.js +36 -0
  33. package/lib/evaluator/probe-worker.js +16 -4
  34. package/lib/loopback.d.ts +6 -0
  35. package/lib/loopback.js +6 -0
  36. package/lib/mcp-core.d.ts +210 -0
  37. package/lib/mcp-core.js +279 -0
  38. package/lib/mcp-engine.d.ts +59 -0
  39. package/lib/mcp-engine.js +289 -0
  40. package/lib/mcp-manifest.d.ts +15 -0
  41. package/lib/mcp-manifest.js +65 -0
  42. package/lib/mcp.d.ts +5 -0
  43. package/lib/mcp.js +2 -0
  44. package/lib/render-service-host.js +1 -1
  45. package/lib/tools/discovery.d.ts +1 -1
  46. package/lib/tools/discovery.js +11 -9
  47. package/lib/tools/materials.d.ts +1 -1
  48. package/lib/tools/materials.js +44 -14
  49. package/lib/tools/operation-contract.d.ts +8 -0
  50. package/lib/tools/operation-contract.js +37 -0
  51. package/lib/tools/operations.d.ts +3 -0
  52. package/lib/tools/operations.js +154 -0
  53. package/lib/tools/registry.d.ts +6 -2
  54. package/lib/tools/registry.js +71 -23
  55. package/package.json +23 -1
  56. package/plugin.json +1 -1
  57. package/plugins/kiln-engine/.claude-plugin/icon.png +0 -0
  58. package/plugins/kiln-engine/.claude-plugin/plugin.json +9 -3
  59. package/plugins/kiln-engine/PRIVACY.md +100 -0
  60. package/plugins/kiln-engine/README.md +103 -18
  61. package/plugins/kiln-engine/{scripts/setup-workspace.mjs → bin/kiln-setup-workspace.mjs} +37 -9
  62. package/plugins/kiln-engine/package-provenance.json +15 -8
  63. package/plugins/kiln-engine/plugin.json +10 -4
  64. package/plugins/kiln-engine/runtime.json +10 -1
  65. package/plugins/kiln-engine/skills/kiln-setup-workspace/SKILL.md +17 -18
  66. package/plugins/kiln-engine/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
  67. package/plugins/kiln-engine/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
  68. package/scripts/create-workspace.mjs +71 -13
  69. package/scripts/workspace-config.mjs +239 -0
  70. package/scripts/workspace-project.mjs +384 -0
  71. package/scripts/workspace-transaction.mjs +504 -0
  72. package/skills/kiln-setup-workspace/SKILL.md +17 -18
  73. package/skills/kiln-setup-workspace/references/plugin-install.md +28 -8
  74. package/skills/kiln-setup-workspace/references/renderer-setup.md +11 -0
  75. package/src/cli-render-mode.ts +9 -9
  76. package/src/cli.ts +3 -3
  77. package/src/discovery/index.ts +2 -0
  78. package/src/discovery/portable.ts +9 -0
  79. package/src/discovery/service.ts +3 -1
  80. package/src/engine-identity.ts +1 -1
  81. package/src/evaluator/isolation.ts +27 -6
  82. package/src/evaluator/probe-invariants.ts +50 -0
  83. package/src/evaluator/probe-worker.ts +22 -4
  84. package/src/mcp-core.ts +3 -0
  85. package/src/mcp-engine.ts +11 -1
  86. package/src/mcp.ts +5 -0
  87. package/src/render-service-host.ts +1 -1
  88. package/src/tools/discovery.ts +20 -10
  89. package/src/tools/materials.ts +59 -15
  90. package/src/tools/operation-contract.ts +51 -0
  91. package/src/tools/operations.ts +177 -0
  92. 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
@@ -0,0 +1,2 @@
1
+ /** Node embedding API. The stdio executable retains its separate lazy entry. */
2
+ export { createKilnMcpServer, createKilnToolHost, kilnMcpToolDefs } from "./mcp-engine.js";
@@ -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().split('\n').slice(-3).join(' ')}` : windowsLaunch ? '; launch the renderer manually to inspect native-driver errors' : ''}`);
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\``);
@@ -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;
@@ -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
- configured: configuredProject(context),
96
- select: 'projectId per call; omitted uses the configured project, null selects standalone work',
97
- read: 'kiln_project { action: "get", projectId } returns the brief, design, inventory and material pins',
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
  },
@@ -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;
@@ -11,13 +11,39 @@ const MATERIAL_ACTIONS = [
11
11
  'create-procedural',
12
12
  'import',
13
13
  ];
14
- const REQUIREMENTS = {
15
- presets: { required: [] },
16
- 'create-preset': { required: ['presetId', 'seed', 'creator', 'license'] },
17
- list: { required: [] },
18
- get: { required: ['materialId'] },
19
- 'create-procedural': { required: ['draft'], shapes: { draft: 'shape:material-draft' } },
20
- import: { required: ['payload'], shapes: { payload: 'shape:material-import' } },
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}` : ''}. kiln_material { action: 'list' } lists the materials in this workspace with their revision IDs.`);
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, REQUIREMENTS[input.action]);
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,3 @@
1
+ import type { KilnToolContext, KilnToolDef } from "./registry.js";
2
+ /** Adapt domain operations in the engine, never in a transport-specific skin. */
3
+ export declare function projectOperationDefs(defs: readonly KilnToolDef[], context: KilnToolContext): KilnToolDef[];
@@ -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
+ }
@@ -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 {};