@instruktlabs/kiln 1.1.0 → 1.2.1
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/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +28 -0
- package/dist/agent-run.mjs +224 -151
- package/dist/build.json +22 -22
- package/dist/cli.mjs +229 -151
- package/dist/mcp-engine.mjs +400 -147
- package/dist/mcp-server.mjs +6 -6
- package/dist/viewer/app.js +180 -180
- package/dist/viewer/chat.html +183 -183
- package/docs/sdk.md +23 -0
- package/docs/tools-operations.md +1529 -0
- package/lib/asset-widget-uri.d.ts +1 -1
- package/lib/asset-widget-uri.js +1 -1
- package/lib/discovery/index.d.ts +1 -1
- package/lib/discovery/index.js +2 -2
- 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/mcp-core.d.ts +2 -1
- package/lib/mcp-core.js +1 -0
- package/lib/mcp-engine.d.ts +2 -0
- package/lib/mcp-engine.js +8 -3
- 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 +7 -4
- package/lib/tools/registry.js +73 -24
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
- package/plugins/kiln-engine/README.md +3 -3
- package/plugins/kiln-engine/package-provenance.json +6 -6
- package/plugins/kiln-engine/plugin.json +1 -1
- package/plugins/kiln-engine/runtime.json +1 -1
- package/src/asset-widget-uri.ts +1 -1
- package/src/discovery/index.ts +2 -0
- package/src/discovery/service.ts +3 -1
- package/src/engine-identity.ts +1 -1
- package/src/generated/mcp-manifest.json +4 -4
- package/src/mcp-core.ts +3 -0
- package/src/mcp-engine.ts +11 -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 +102 -21
- package/src/viewer/chat-app.ts +6 -2
- package/src/viewer/glb-loader.ts +78 -0
- package/src/viewer/scene.ts +9 -26
package/src/tools/materials.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { createMaterialRecordV1, decodeMaterialLibraryPayload } from '../materia
|
|
|
14
14
|
import { createMaterialPresetDraft, listMaterialPresets } from '../material-presets';
|
|
15
15
|
import { nestedRecordDescription, requireActionFields } from './actions';
|
|
16
16
|
import type { KilnToolDef } from './registry';
|
|
17
|
+
import type { KilnActionContract } from './operation-contract';
|
|
17
18
|
|
|
18
19
|
const MATERIAL_ACTIONS = [
|
|
19
20
|
'presets',
|
|
@@ -24,13 +25,43 @@ const MATERIAL_ACTIONS = [
|
|
|
24
25
|
'import',
|
|
25
26
|
] as const;
|
|
26
27
|
type MaterialAction = (typeof MATERIAL_ACTIONS)[number];
|
|
27
|
-
const
|
|
28
|
-
presets: {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const MATERIAL_CONTRACT: Record<MaterialAction, KilnActionContract> = {
|
|
29
|
+
presets: {
|
|
30
|
+
fields: ['tag'],
|
|
31
|
+
required: [],
|
|
32
|
+
description: 'List shipped material recipes, optionally filtered by tag.',
|
|
33
|
+
},
|
|
34
|
+
'create-preset': {
|
|
35
|
+
fields: ['presetId', 'seed', 'creator', 'license', 'size', 'materialId'],
|
|
36
|
+
required: ['presetId', 'seed', 'creator', 'license'],
|
|
37
|
+
description:
|
|
38
|
+
'Create an immutable material from a shipped preset with explicit seed, creator and license.',
|
|
39
|
+
},
|
|
40
|
+
list: {
|
|
41
|
+
fields: ['tag'],
|
|
42
|
+
required: [],
|
|
43
|
+
description: 'List saved immutable material revisions, optionally filtered by tag.',
|
|
44
|
+
},
|
|
45
|
+
get: {
|
|
46
|
+
fields: ['materialId', 'revisionId'],
|
|
47
|
+
required: ['materialId'],
|
|
48
|
+
description:
|
|
49
|
+
'Read a material revision, provenance and portable specification. Omit revisionId only when exactly one revision exists.',
|
|
50
|
+
},
|
|
51
|
+
'create-procedural': {
|
|
52
|
+
fields: ['draft'],
|
|
53
|
+
required: ['draft'],
|
|
54
|
+
shapes: { draft: 'shape:material-draft' },
|
|
55
|
+
description:
|
|
56
|
+
'Create an immutable material from a bounded procedural draft. Does not fetch URLs or execute source.',
|
|
57
|
+
},
|
|
58
|
+
import: {
|
|
59
|
+
fields: ['payload'],
|
|
60
|
+
required: ['payload'],
|
|
61
|
+
shapes: { payload: 'shape:material-import' },
|
|
62
|
+
description:
|
|
63
|
+
'Import complete normalized material records and embedded PNGs. Does not fetch URLs or execute source.',
|
|
64
|
+
},
|
|
34
65
|
};
|
|
35
66
|
const record = (description: string) =>
|
|
36
67
|
z.record(z.string(), z.unknown()).optional().describe(description);
|
|
@@ -74,9 +105,9 @@ const materialResult = (material: MaterialManifestV1) => ({
|
|
|
74
105
|
material,
|
|
75
106
|
portableSpec: materialLibraryPortableSpec(material),
|
|
76
107
|
});
|
|
77
|
-
const unknownMaterial = (materialId: string, revisionId
|
|
108
|
+
const unknownMaterial = (materialId: string, revisionId: string | undefined, listCall: string) =>
|
|
78
109
|
new Error(
|
|
79
|
-
`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}.
|
|
110
|
+
`Unknown material ${materialId}${revisionId ? ` at ${revisionId}` : ''}. ${listCall} lists the materials in this workspace with their revision IDs.`,
|
|
80
111
|
);
|
|
81
112
|
/**
|
|
82
113
|
* The revision a `get` without `revisionId` means: the material's only one.
|
|
@@ -84,13 +115,17 @@ const unknownMaterial = (materialId: string, revisionId?: string) =>
|
|
|
84
115
|
* caller to choose (two live sessions of 1 October 2026 called get with the
|
|
85
116
|
* id alone and were refused).
|
|
86
117
|
*/
|
|
87
|
-
async function onlyRevision(
|
|
118
|
+
async function onlyRevision(
|
|
119
|
+
library: MaterialLibrary,
|
|
120
|
+
materialId: string,
|
|
121
|
+
listCall: string,
|
|
122
|
+
): Promise<string> {
|
|
88
123
|
const revisions = (await library.list())
|
|
89
124
|
.filter((material) => material.materialId === materialId)
|
|
90
125
|
.map((material) => material.revisionId)
|
|
91
126
|
.sort();
|
|
92
127
|
if (revisions.length === 1) return revisions[0]!;
|
|
93
|
-
if (revisions.length === 0) throw unknownMaterial(materialId);
|
|
128
|
+
if (revisions.length === 0) throw unknownMaterial(materialId, undefined, listCall);
|
|
94
129
|
throw new Error(
|
|
95
130
|
`Material ${materialId} has ${revisions.length} revisions: ${revisions.join(', ')}. Pass revisionId to get one.`,
|
|
96
131
|
);
|
|
@@ -111,12 +146,20 @@ function aliasMaterialId(raw: unknown): unknown {
|
|
|
111
146
|
);
|
|
112
147
|
return { ...rest, materialId: resourceId };
|
|
113
148
|
}
|
|
114
|
-
export function createKilnMaterialDef(
|
|
149
|
+
export function createKilnMaterialDef(
|
|
150
|
+
library: MaterialLibrary,
|
|
151
|
+
presentation: 'grouped' | 'operations' = 'grouped',
|
|
152
|
+
): KilnToolDef {
|
|
153
|
+
const listCall =
|
|
154
|
+
presentation === 'operations'
|
|
155
|
+
? 'kiln_material_search({ scope: "saved" })'
|
|
156
|
+
: "kiln_material { action: 'list' }";
|
|
115
157
|
return {
|
|
116
158
|
name: 'kiln_material',
|
|
117
159
|
description:
|
|
118
160
|
'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.',
|
|
119
161
|
inputSchema: materialToolInput,
|
|
162
|
+
actionContract: MATERIAL_CONTRACT,
|
|
120
163
|
annotations: {
|
|
121
164
|
readOnlyHint: false,
|
|
122
165
|
destructiveHint: false,
|
|
@@ -126,7 +169,7 @@ export function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef {
|
|
|
126
169
|
async run(raw) {
|
|
127
170
|
assertMaterialJson(raw);
|
|
128
171
|
const input = materialToolInput.parse(aliasMaterialId(raw));
|
|
129
|
-
requireActionFields('kiln_material', input.action, input,
|
|
172
|
+
requireActionFields('kiln_material', input.action, input, MATERIAL_CONTRACT[input.action]);
|
|
130
173
|
switch (input.action) {
|
|
131
174
|
case 'presets':
|
|
132
175
|
return {
|
|
@@ -170,13 +213,14 @@ export function createKilnMaterialDef(library: MaterialLibrary): KilnToolDef {
|
|
|
170
213
|
};
|
|
171
214
|
}
|
|
172
215
|
case 'get': {
|
|
173
|
-
const revisionId =
|
|
216
|
+
const revisionId =
|
|
217
|
+
input.revisionId ?? (await onlyRevision(library, input.materialId!, listCall));
|
|
174
218
|
try {
|
|
175
219
|
return materialResult((await library.read(input.materialId!, revisionId)).manifest);
|
|
176
220
|
} catch (error) {
|
|
177
221
|
// Rule 9: an unknown id names the call that lists the known ones.
|
|
178
222
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT')
|
|
179
|
-
throw unknownMaterial(input.materialId!, revisionId);
|
|
223
|
+
throw unknownMaterial(input.materialId!, revisionId, listCall);
|
|
180
224
|
throw error;
|
|
181
225
|
}
|
|
182
226
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ActionRequirements } from './actions';
|
|
3
|
+
|
|
4
|
+
/** Domain action metadata shared by grouped execution and named presentations. */
|
|
5
|
+
export interface KilnActionContract extends ActionRequirements {
|
|
6
|
+
readonly fields: readonly string[];
|
|
7
|
+
readonly description: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function projectActionSchemas<A extends string>(
|
|
11
|
+
name: string,
|
|
12
|
+
input: z.ZodObject,
|
|
13
|
+
actions: Readonly<Record<A, KilnActionContract>>,
|
|
14
|
+
): Record<A, z.ZodObject> {
|
|
15
|
+
if (input.def.checks?.length)
|
|
16
|
+
throw new Error(`${name}: cross-field constraints require an explicit operation schema`);
|
|
17
|
+
const json = z.toJSONSchema(input, { io: 'input' });
|
|
18
|
+
const selector = json.properties?.action as { enum?: unknown } | undefined;
|
|
19
|
+
const expected = Object.keys(actions).sort();
|
|
20
|
+
if (
|
|
21
|
+
!Array.isArray(selector?.enum) ||
|
|
22
|
+
JSON.stringify([...selector.enum].sort()) !== JSON.stringify(expected)
|
|
23
|
+
)
|
|
24
|
+
throw new Error(`${name}: unclassified actions`);
|
|
25
|
+
const classified = new Set([
|
|
26
|
+
'action',
|
|
27
|
+
...Object.values<KilnActionContract>(actions).flatMap((contract) => [...contract.fields]),
|
|
28
|
+
]);
|
|
29
|
+
const unclassified = Object.keys(input.shape).filter((field) => !classified.has(field));
|
|
30
|
+
if (unclassified.length)
|
|
31
|
+
throw new Error(`${name}: Unclassified fields: ${unclassified.join(', ')}`);
|
|
32
|
+
const schemas: [string, z.ZodObject][] = [];
|
|
33
|
+
for (const [action, contract] of Object.entries<KilnActionContract>(actions)) {
|
|
34
|
+
for (const field of contract.required)
|
|
35
|
+
if (!contract.fields.includes(field))
|
|
36
|
+
throw new Error(`${name}.${action}: unmapped required ${field}`);
|
|
37
|
+
for (const field of json.required ?? [])
|
|
38
|
+
if (field !== 'action' && !contract.fields.includes(field))
|
|
39
|
+
throw new Error(`${name}.${action}: would omit required ${field}`);
|
|
40
|
+
const shape: Record<string, z.ZodType> = {};
|
|
41
|
+
for (const field of contract.fields) {
|
|
42
|
+
if (field === 'action' || !Object.hasOwn(input.shape, field))
|
|
43
|
+
throw new Error(`${name}.${action}: missing or reserved field ${field}`);
|
|
44
|
+
classified.add(field);
|
|
45
|
+
const source = input.shape[field]!;
|
|
46
|
+
shape[field] = contract.required.includes(field) ? source.nonoptional() : source;
|
|
47
|
+
}
|
|
48
|
+
schemas.push([action, z.strictObject(shape)]);
|
|
49
|
+
}
|
|
50
|
+
return Object.fromEntries(schemas) as Record<A, z.ZodObject>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { KilnToolContext, KilnToolDef } from './registry';
|
|
3
|
+
import { projectActionSchemas } from './operation-contract';
|
|
4
|
+
|
|
5
|
+
/** Adapt domain operations in the engine, never in a transport-specific skin. */
|
|
6
|
+
export function projectOperationDefs(
|
|
7
|
+
defs: readonly KilnToolDef[],
|
|
8
|
+
context: KilnToolContext,
|
|
9
|
+
): KilnToolDef[] {
|
|
10
|
+
return defs.flatMap((def) => {
|
|
11
|
+
if (
|
|
12
|
+
!context.assetLibrary &&
|
|
13
|
+
['kiln_save', 'kiln_assets', 'kiln_present', 'kiln_export', 'kiln_import'].includes(def.name)
|
|
14
|
+
)
|
|
15
|
+
return [];
|
|
16
|
+
if (def.name === 'kiln_renderer') return rendererOperations(def, context);
|
|
17
|
+
if (def.name === 'kiln_discover') return discoveryOperations(def);
|
|
18
|
+
if (def.name === 'kiln_material') return materialOperations(def);
|
|
19
|
+
if (def.name === 'kiln_assets') return assetOperations(def, context);
|
|
20
|
+
return [def];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function actionTools(def: KilnToolDef) {
|
|
25
|
+
if (!(def.inputSchema instanceof z.ZodObject) || !def.actionContract)
|
|
26
|
+
throw new Error(`${def.name}: missing shared action contract`);
|
|
27
|
+
const schemas = projectActionSchemas(def.name, def.inputSchema, def.actionContract);
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(def.actionContract).map(([action, contract]) => [
|
|
30
|
+
action,
|
|
31
|
+
{
|
|
32
|
+
...def,
|
|
33
|
+
actionContract: undefined,
|
|
34
|
+
name: `${def.name}_${action.replaceAll('-', '_')}`,
|
|
35
|
+
description: contract.description,
|
|
36
|
+
inputSchema: schemas[action]!,
|
|
37
|
+
run: async (raw: unknown) => def.run({ ...schemas[action]!.parse(raw), action }),
|
|
38
|
+
} satisfies KilnToolDef,
|
|
39
|
+
]),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function assetOperations(def: KilnToolDef, context: KilnToolContext): KilnToolDef[] {
|
|
44
|
+
const tools = actionTools(def);
|
|
45
|
+
const list = tools.list!;
|
|
46
|
+
const listSchema = list.inputSchema as z.ZodObject;
|
|
47
|
+
// Remove only the legacy default: omission means all collections in this operation.
|
|
48
|
+
const collection = (listSchema.shape.collection as z.ZodDefault<z.ZodString>)
|
|
49
|
+
.unwrap()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe(
|
|
52
|
+
'Collection ID from kiln_assets_collections; omitted searches all configured collections.',
|
|
53
|
+
);
|
|
54
|
+
const inputSchema = listSchema.extend({ collection });
|
|
55
|
+
const search: KilnToolDef = {
|
|
56
|
+
...list,
|
|
57
|
+
name: 'kiln_assets_search',
|
|
58
|
+
description:
|
|
59
|
+
'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.',
|
|
60
|
+
inputSchema,
|
|
61
|
+
run: async (raw) => {
|
|
62
|
+
const input = inputSchema.parse(raw);
|
|
63
|
+
const { collection: _collection, ...filters } = input;
|
|
64
|
+
if (input.collection === undefined) return tools.catalog!.run(filters);
|
|
65
|
+
if (!context.assetLibrary?.collections().some((item) => item.id === input.collection))
|
|
66
|
+
throw new Error(
|
|
67
|
+
'Unknown collection. Use kiln_assets_collections to list available destinations.',
|
|
68
|
+
);
|
|
69
|
+
const result = (await list.run(input)) as {
|
|
70
|
+
total: number;
|
|
71
|
+
nextOffset: number | null;
|
|
72
|
+
assets: Record<string, unknown>[];
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
errors: [],
|
|
76
|
+
total: result.total,
|
|
77
|
+
nextOffset: result.nextOffset,
|
|
78
|
+
assets: result.assets.map((asset) => ({ ...asset, collection: input.collection })),
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
return [tools.collections!, search, tools.get!, tools.restore!];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function materialOperations(def: KilnToolDef): KilnToolDef[] {
|
|
86
|
+
const tools = actionTools(def);
|
|
87
|
+
const presets = tools.presets!;
|
|
88
|
+
const inputSchema = (presets.inputSchema as z.ZodObject).extend({
|
|
89
|
+
scope: z.enum(['all', 'presets', 'saved']).default('all'),
|
|
90
|
+
});
|
|
91
|
+
const search: KilnToolDef = {
|
|
92
|
+
...presets,
|
|
93
|
+
name: 'kiln_material_search',
|
|
94
|
+
description:
|
|
95
|
+
'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.',
|
|
96
|
+
inputSchema,
|
|
97
|
+
run: async (raw) => {
|
|
98
|
+
const { scope, ...input } = inputSchema.parse(raw);
|
|
99
|
+
const recipes =
|
|
100
|
+
scope === 'saved'
|
|
101
|
+
? { presets: [] }
|
|
102
|
+
: ((await presets.run(input)) as { presets: Record<string, unknown>[] });
|
|
103
|
+
const saved =
|
|
104
|
+
scope === 'presets'
|
|
105
|
+
? { materials: [] }
|
|
106
|
+
: ((await tools.list!.run(input)) as { materials: Record<string, unknown>[] });
|
|
107
|
+
return {
|
|
108
|
+
ok: true,
|
|
109
|
+
presets: recipes.presets.map((item) => ({ ...item, kind: 'preset' })),
|
|
110
|
+
materials: saved.materials.map((item) => ({ ...item, kind: 'saved' })),
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
const preset = tools['create-preset']!;
|
|
115
|
+
const procedural = tools['create-procedural']!;
|
|
116
|
+
const createInput = z.strictObject({
|
|
117
|
+
definition: z.discriminatedUnion('kind', [
|
|
118
|
+
(preset.inputSchema as z.ZodObject).extend({ kind: z.literal('preset') }),
|
|
119
|
+
(procedural.inputSchema as z.ZodObject).extend({ kind: z.literal('procedural') }),
|
|
120
|
+
]),
|
|
121
|
+
});
|
|
122
|
+
const create: KilnToolDef = {
|
|
123
|
+
...preset,
|
|
124
|
+
name: 'kiln_material_create',
|
|
125
|
+
description:
|
|
126
|
+
'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.',
|
|
127
|
+
inputSchema: createInput,
|
|
128
|
+
run: async (raw) => {
|
|
129
|
+
const { kind, ...input } = createInput.parse(raw).definition;
|
|
130
|
+
return (kind === 'preset' ? preset : procedural).run(input);
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
return [search, tools.get!, create, tools.import!];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rendererOperations(def: KilnToolDef, context: KilnToolContext): KilnToolDef[] {
|
|
137
|
+
if (!context.renderCapabilities) return [];
|
|
138
|
+
return ['status', ...(context.reprobeRenderer ? ['reprobe'] : [])].map((action) => {
|
|
139
|
+
const inputSchema = z.strictObject({});
|
|
140
|
+
return {
|
|
141
|
+
...def,
|
|
142
|
+
name: `kiln_renderer_${action}`,
|
|
143
|
+
inputSchema,
|
|
144
|
+
run: async (raw: unknown) => {
|
|
145
|
+
inputSchema.parse(raw);
|
|
146
|
+
return def.run({ action });
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function discoveryOperations(def: KilnToolDef): KilnToolDef[] {
|
|
153
|
+
const original = def.inputSchema as z.ZodObject;
|
|
154
|
+
const { capabilities: _capabilities, ...fields } = original.shape;
|
|
155
|
+
const inputSchema = z.strictObject(fields);
|
|
156
|
+
const empty = z.strictObject({});
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
...def,
|
|
160
|
+
description:
|
|
161
|
+
'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.',
|
|
162
|
+
inputSchema,
|
|
163
|
+
run: async (raw) => def.run(inputSchema.parse(raw)),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
...def,
|
|
167
|
+
name: 'kiln_capabilities',
|
|
168
|
+
description:
|
|
169
|
+
'Inspect declared host capabilities, execution limits and available storage. Observed rendering fidelity is returned by each render.',
|
|
170
|
+
inputSchema: empty,
|
|
171
|
+
run: async (raw) => {
|
|
172
|
+
empty.parse(raw);
|
|
173
|
+
return def.run({ capabilities: true });
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
}
|
package/src/tools/registry.ts
CHANGED
|
@@ -24,6 +24,8 @@ import { createKilnMaterialDef } from './materials';
|
|
|
24
24
|
import { createKilnReviewDef, type ReviewStore } from './review';
|
|
25
25
|
import { withWorkspaceContext } from './workspace';
|
|
26
26
|
import { describeInputError } from './actions';
|
|
27
|
+
import { projectOperationDefs } from './operations';
|
|
28
|
+
import type { KilnActionContract } from './operation-contract';
|
|
27
29
|
import {
|
|
28
30
|
advancedCaptureInput,
|
|
29
31
|
cameraShotInput,
|
|
@@ -92,8 +94,11 @@ import type { TextureUsage } from '../textures';
|
|
|
92
94
|
// Tool definition contract
|
|
93
95
|
// =============================================================================
|
|
94
96
|
|
|
95
|
-
export
|
|
97
|
+
export { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
|
|
98
|
+
import { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
|
|
96
99
|
export interface KilnToolDef {
|
|
100
|
+
/** Shared action ownership and requirements; never hand-maintained by transports. */
|
|
101
|
+
actionContract?: Readonly<Record<string, import('./operation-contract').KilnActionContract>>;
|
|
97
102
|
outputSchema?: z.ZodType;
|
|
98
103
|
/** Optional MCP App presentation; metadata is hidden from the language model. */
|
|
99
104
|
ui?: {
|
|
@@ -2498,13 +2503,14 @@ async function readSavedRevision(
|
|
|
2498
2503
|
collection: string,
|
|
2499
2504
|
assetId: string,
|
|
2500
2505
|
revisionId: string,
|
|
2506
|
+
listCall = `kiln_assets { action: 'list', collection: '${collection}' }`,
|
|
2501
2507
|
) {
|
|
2502
2508
|
try {
|
|
2503
2509
|
return await target.read(collection, assetId, revisionId);
|
|
2504
2510
|
} catch (error) {
|
|
2505
2511
|
if ((error as NodeJS.ErrnoException).code === 'ENOENT')
|
|
2506
2512
|
throw new Error(
|
|
2507
|
-
`Unknown asset ${assetId} revision ${revisionId} in collection ${collection}.
|
|
2513
|
+
`Unknown asset ${assetId} revision ${revisionId} in collection ${collection}. ${listCall} lists the saved revisions.`,
|
|
2508
2514
|
);
|
|
2509
2515
|
throw error;
|
|
2510
2516
|
}
|
|
@@ -2512,12 +2518,19 @@ async function readSavedRevision(
|
|
|
2512
2518
|
|
|
2513
2519
|
export function createKilnProgramToolRegistry(
|
|
2514
2520
|
suppliedContext: KilnToolContext = {},
|
|
2521
|
+
): KilnToolDef[] {
|
|
2522
|
+
return createProgramDefs(suppliedContext, 'grouped');
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
function createProgramDefs(
|
|
2526
|
+
suppliedContext: KilnToolContext,
|
|
2527
|
+
presentation: 'grouped' | 'operations',
|
|
2515
2528
|
): KilnToolDef[] {
|
|
2516
2529
|
toolRequirements(suppliedContext);
|
|
2517
2530
|
const store = suppliedContext.programStore ?? new MemoryProgramStore();
|
|
2518
2531
|
const context = withCaptureCache(withBuildCache({ ...suppliedContext, programStore: store }));
|
|
2519
2532
|
return [
|
|
2520
|
-
createKilnDiscoveryDef({ ...suppliedContext, programStore: store }),
|
|
2533
|
+
createKilnDiscoveryDef({ ...suppliedContext, programStore: store }, presentation),
|
|
2521
2534
|
{
|
|
2522
2535
|
name: 'kiln_renderer',
|
|
2523
2536
|
description:
|
|
@@ -2564,7 +2577,9 @@ export function createKilnProgramToolRegistry(
|
|
|
2564
2577
|
...(context.projectStore
|
|
2565
2578
|
? [createKilnProjectDef(context.projectStore, context.projectBundleReader)]
|
|
2566
2579
|
: []),
|
|
2567
|
-
...(context.materialLibrary
|
|
2580
|
+
...(context.materialLibrary
|
|
2581
|
+
? [createKilnMaterialDef(context.materialLibrary, presentation)]
|
|
2582
|
+
: []),
|
|
2568
2583
|
...(context.reviewStore && context.assetLibrary
|
|
2569
2584
|
? [
|
|
2570
2585
|
createKilnReviewDef({
|
|
@@ -2574,7 +2589,7 @@ export function createKilnProgramToolRegistry(
|
|
|
2574
2589
|
}),
|
|
2575
2590
|
]
|
|
2576
2591
|
: []),
|
|
2577
|
-
...createKilnAssetDefs({ ...context, programStore: store }),
|
|
2592
|
+
...createKilnAssetDefs({ ...context, programStore: store }, presentation),
|
|
2578
2593
|
]
|
|
2579
2594
|
.map((def) => withWorkspaceContext(def, context))
|
|
2580
2595
|
.map((def) => ({
|
|
@@ -2591,6 +2606,11 @@ export function createKilnProgramToolRegistry(
|
|
|
2591
2606
|
}));
|
|
2592
2607
|
}
|
|
2593
2608
|
|
|
2609
|
+
/** Opt-in named domain operations; grouped local/native contracts remain unchanged. */
|
|
2610
|
+
export function createKilnOperationToolRegistry(context: KilnToolContext = {}): KilnToolDef[] {
|
|
2611
|
+
return projectOperationDefs(createProgramDefs(context, 'operations'), context);
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2594
2614
|
/** Canonical native workflow: shared definitions plus one host-owned terminal. */
|
|
2595
2615
|
export function createKilnNativeToolRegistry(
|
|
2596
2616
|
suppliedContext: KilnToolContext,
|
|
@@ -2686,6 +2706,36 @@ const assetSelector = {
|
|
|
2686
2706
|
revisionId: z.string().regex(/^[a-z][a-z0-9_-]{0,79}$/),
|
|
2687
2707
|
};
|
|
2688
2708
|
|
|
2709
|
+
const ASSET_CONTRACT = {
|
|
2710
|
+
collections: {
|
|
2711
|
+
fields: [],
|
|
2712
|
+
required: [],
|
|
2713
|
+
description: 'List configured asset storage collections. Collection is not project membership.',
|
|
2714
|
+
},
|
|
2715
|
+
catalog: {
|
|
2716
|
+
fields: ['assetId', 'query', 'offset', 'limit'],
|
|
2717
|
+
required: [],
|
|
2718
|
+
description: 'Search saved assets across configured collections with pagination.',
|
|
2719
|
+
},
|
|
2720
|
+
list: {
|
|
2721
|
+
fields: ['collection', 'assetId', 'query', 'offset', 'limit'],
|
|
2722
|
+
required: [],
|
|
2723
|
+
description: 'Search saved assets in one collection with pagination.',
|
|
2724
|
+
},
|
|
2725
|
+
get: {
|
|
2726
|
+
fields: ['collection', 'assetId', 'revisionId'],
|
|
2727
|
+
required: ['assetId'],
|
|
2728
|
+
description:
|
|
2729
|
+
'Read a saved revision and its download descriptors. Omit revisionId for the newest revision.',
|
|
2730
|
+
},
|
|
2731
|
+
restore: {
|
|
2732
|
+
fields: ['collection', 'assetId', 'revisionId'],
|
|
2733
|
+
required: ['assetId'],
|
|
2734
|
+
description:
|
|
2735
|
+
'Restore exact saved source as programRef for editing or rendering. Omit revisionId for the newest revision. Does not overwrite the saved revision. Binary-only assets have no source.',
|
|
2736
|
+
},
|
|
2737
|
+
} satisfies Record<string, KilnActionContract>;
|
|
2738
|
+
|
|
2689
2739
|
/**
|
|
2690
2740
|
* The revision a get or restore without `revisionId` means: the newest, the
|
|
2691
2741
|
* one no later revision names as its parent, ties to the latest `createdAt`
|
|
@@ -2701,12 +2751,11 @@ export async function newestRevision(
|
|
|
2701
2751
|
},
|
|
2702
2752
|
collection: string,
|
|
2703
2753
|
assetId: string,
|
|
2754
|
+
listCall = `kiln_assets { action: 'list', collection: '${collection}' }`,
|
|
2704
2755
|
): Promise<string> {
|
|
2705
2756
|
const revisions = (await target.list(collection)).filter((a) => a.assetId === assetId);
|
|
2706
2757
|
if (!revisions.length)
|
|
2707
|
-
throw new Error(
|
|
2708
|
-
`Unknown asset ${assetId} in ${collection}; kiln_assets { action: 'list', collection: '${collection}' } lists them.`,
|
|
2709
|
-
);
|
|
2758
|
+
throw new Error(`Unknown asset ${assetId} in ${collection}; ${listCall} lists them.`);
|
|
2710
2759
|
const parents = new Set(revisions.map((a) => a.parentRevision).filter(Boolean));
|
|
2711
2760
|
const heads = revisions.filter((a) => !parents.has(a.revisionId));
|
|
2712
2761
|
const newest = (heads.length ? heads : revisions).sort((a, b) =>
|
|
@@ -2797,7 +2846,23 @@ export async function buildProgramAssetDraft(
|
|
|
2797
2846
|
}
|
|
2798
2847
|
|
|
2799
2848
|
/** All asset operation schemas live here, alongside the existing tool definitions. */
|
|
2800
|
-
export function createKilnAssetDefs(
|
|
2849
|
+
export function createKilnAssetDefs(
|
|
2850
|
+
context: KilnToolContext,
|
|
2851
|
+
presentation: 'grouped' | 'operations' = 'grouped',
|
|
2852
|
+
): KilnToolDef[] {
|
|
2853
|
+
const listCall = (collection: string) =>
|
|
2854
|
+
presentation === 'operations'
|
|
2855
|
+
? `kiln_assets_search({ collection: '${collection}' })`
|
|
2856
|
+
: `kiln_assets { action: 'list', collection: '${collection}' }`;
|
|
2857
|
+
const selector =
|
|
2858
|
+
presentation === 'operations'
|
|
2859
|
+
? {
|
|
2860
|
+
...assetSelector,
|
|
2861
|
+
collection: assetSelector.collection.describe(
|
|
2862
|
+
'Collection ID from kiln_assets_collections; default project.',
|
|
2863
|
+
),
|
|
2864
|
+
}
|
|
2865
|
+
: assetSelector;
|
|
2801
2866
|
const library = () => {
|
|
2802
2867
|
if (!context.assetLibrary)
|
|
2803
2868
|
throw new Error(
|
|
@@ -2828,11 +2893,11 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2828
2893
|
downloadUrls: await context.assetDownloadUrls?.(collection, asset.assetId, asset.revisionId),
|
|
2829
2894
|
});
|
|
2830
2895
|
const saveInput = z.object({
|
|
2831
|
-
collection:
|
|
2896
|
+
collection: selector.collection,
|
|
2832
2897
|
programRef: z.string(),
|
|
2833
2898
|
name: z.string().min(1).max(200),
|
|
2834
|
-
assetId:
|
|
2835
|
-
parentRevision:
|
|
2899
|
+
assetId: selector.assetId.optional(),
|
|
2900
|
+
parentRevision: selector.revisionId.optional(),
|
|
2836
2901
|
tags: z.array(z.string().max(80)).max(30).optional(),
|
|
2837
2902
|
brief: z.string().max(8000).optional(),
|
|
2838
2903
|
description: z.string().max(4000).optional(),
|
|
@@ -2850,16 +2915,16 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2850
2915
|
});
|
|
2851
2916
|
const assetsInput = z.object({
|
|
2852
2917
|
action: z.enum(['collections', 'catalog', 'list', 'get', 'restore']).default('list'),
|
|
2853
|
-
collection:
|
|
2854
|
-
assetId:
|
|
2855
|
-
revisionId:
|
|
2918
|
+
collection: selector.collection,
|
|
2919
|
+
assetId: selector.assetId.optional(),
|
|
2920
|
+
revisionId: selector.revisionId
|
|
2856
2921
|
.optional()
|
|
2857
2922
|
.describe('get, restore: the saved revision; omitted, the newest.'),
|
|
2858
2923
|
query: z.string().max(200).optional(),
|
|
2859
2924
|
offset: z.number().int().min(0).default(0),
|
|
2860
2925
|
limit: z.number().int().min(1).max(50).default(20),
|
|
2861
2926
|
});
|
|
2862
|
-
const exportInput = z.object(
|
|
2927
|
+
const exportInput = z.object(selector);
|
|
2863
2928
|
const profileExportInput = exportInput.extend({
|
|
2864
2929
|
profile: z
|
|
2865
2930
|
.enum(['editable', 'runtime'])
|
|
@@ -2869,8 +2934,8 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2869
2934
|
),
|
|
2870
2935
|
});
|
|
2871
2936
|
const importInput = z.object({
|
|
2872
|
-
...
|
|
2873
|
-
sourceCollection:
|
|
2937
|
+
...selector,
|
|
2938
|
+
sourceCollection: selector.collection,
|
|
2874
2939
|
});
|
|
2875
2940
|
return [
|
|
2876
2941
|
{
|
|
@@ -2892,6 +2957,7 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2892
2957
|
input.collection,
|
|
2893
2958
|
input.assetId,
|
|
2894
2959
|
input.parentRevision,
|
|
2960
|
+
listCall(input.collection),
|
|
2895
2961
|
);
|
|
2896
2962
|
assertSavedRequirementsAuthorized(previous.manifest, activeRequirements);
|
|
2897
2963
|
}
|
|
@@ -2908,6 +2974,7 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2908
2974
|
description:
|
|
2909
2975
|
'Browse saved assets: collections discovers storage; catalog searches all configured collections; list searches one. Both searches paginate. get returns a build record/downloads; restore loads exact source for kiln_source/kiln_edit. Collection is not project membership. Binary-only assets cannot restore source.',
|
|
2910
2976
|
inputSchema: assetsInput,
|
|
2977
|
+
actionContract: ASSET_CONTRACT,
|
|
2911
2978
|
run: async (raw) => {
|
|
2912
2979
|
const input = assetsInput.parse(raw);
|
|
2913
2980
|
const activeRequirements = toolRequirements(context);
|
|
@@ -2961,13 +3028,25 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
2961
3028
|
})),
|
|
2962
3029
|
};
|
|
2963
3030
|
}
|
|
2964
|
-
if (!input
|
|
3031
|
+
if (ASSET_CONTRACT[input.action].required.some((field) => !input[field as 'assetId']))
|
|
2965
3032
|
throw new Error(
|
|
2966
3033
|
`kiln_assets ${input.action} requires assetId; kiln_assets { action: 'list', collection: '${input.collection}' } lists them.`,
|
|
2967
3034
|
);
|
|
2968
3035
|
const revisionId =
|
|
2969
|
-
input.revisionId ??
|
|
2970
|
-
|
|
3036
|
+
input.revisionId ??
|
|
3037
|
+
(await newestRevision(
|
|
3038
|
+
target,
|
|
3039
|
+
input.collection,
|
|
3040
|
+
input.assetId!,
|
|
3041
|
+
listCall(input.collection),
|
|
3042
|
+
));
|
|
3043
|
+
const record = await readSavedRevision(
|
|
3044
|
+
target,
|
|
3045
|
+
input.collection,
|
|
3046
|
+
input.assetId!,
|
|
3047
|
+
revisionId,
|
|
3048
|
+
listCall(input.collection),
|
|
3049
|
+
);
|
|
2971
3050
|
if (input.action === 'get') return links(input.collection, record.manifest);
|
|
2972
3051
|
const saved = assertSavedRequirementsAuthorized(record.manifest, activeRequirements);
|
|
2973
3052
|
const code = record.files['source.kiln.js'];
|
|
@@ -3059,6 +3138,7 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
3059
3138
|
input.collection,
|
|
3060
3139
|
input.assetId,
|
|
3061
3140
|
input.revisionId,
|
|
3141
|
+
listCall(input.collection),
|
|
3062
3142
|
);
|
|
3063
3143
|
const { inspectDrawDiagnostics } = await import('../draw-diagnostics');
|
|
3064
3144
|
if (input.profile === 'runtime') {
|
|
@@ -3102,6 +3182,7 @@ export function createKilnAssetDefs(context: KilnToolContext): KilnToolDef[] {
|
|
|
3102
3182
|
input.sourceCollection,
|
|
3103
3183
|
input.assetId,
|
|
3104
3184
|
input.revisionId,
|
|
3185
|
+
listCall(input.sourceCollection),
|
|
3105
3186
|
);
|
|
3106
3187
|
await target.import(input.collection, [record]);
|
|
3107
3188
|
return links(input.collection, record.manifest);
|
package/src/viewer/chat-app.ts
CHANGED
|
@@ -51,6 +51,8 @@ function encode(bytes: Uint8Array) {
|
|
|
51
51
|
async function show(input: unknown) {
|
|
52
52
|
const revision = ++presentationRevision;
|
|
53
53
|
record = undefined;
|
|
54
|
+
element('stats').textContent = '';
|
|
55
|
+
element<HTMLSelectElement>('clips').hidden = true;
|
|
54
56
|
for (const button of document.querySelectorAll<HTMLButtonElement>('button[data-download]'))
|
|
55
57
|
button.disabled = true;
|
|
56
58
|
try {
|
|
@@ -116,14 +118,16 @@ async function showResult(
|
|
|
116
118
|
status.textContent = 'Drag to orbit · Scroll to zoom';
|
|
117
119
|
} catch (error) {
|
|
118
120
|
if (revision !== presentationRevision) return;
|
|
121
|
+
stage?.dispose();
|
|
122
|
+
stage = undefined;
|
|
119
123
|
const preview = record.files['preview.png'];
|
|
120
124
|
if (preview) {
|
|
121
125
|
const image = new Image();
|
|
122
|
-
image.alt = record.manifest.name
|
|
126
|
+
image.alt = `${record.manifest.name} · saved preview image`;
|
|
123
127
|
image.src = `data:image/png;base64,${encode(preview)}`;
|
|
124
128
|
element('stage').replaceChildren(image);
|
|
125
129
|
}
|
|
126
|
-
status.textContent =
|
|
130
|
+
status.textContent = `${preview ? 'Showing saved preview. ' : ''}3D preview unavailable: ${String(error)}`;
|
|
127
131
|
}
|
|
128
132
|
for (const button of document.querySelectorAll<HTMLButtonElement>('button[data-download]'))
|
|
129
133
|
button.disabled = false;
|