@instruktlabs/kiln 1.2.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.
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
9
+ export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v6.html";
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
9
+ export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v6.html';
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export declare const ENGINE_VERSION = "1.2.0";
29
+ export declare const ENGINE_VERSION = "1.2.1";
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export const ENGINE_VERSION = '1.2.0';
29
+ export const ENGINE_VERSION = '1.2.1';
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
package/lib/mcp-core.d.ts CHANGED
@@ -23,7 +23,7 @@ import { type ServeStdioOptions, type StdioServerHandle } from '@modelcontextpro
23
23
  import type { AssetLink } from "./assets-resources.js";
24
24
  /** Server identity reported in the handshake and stamped into every 2026-07-28 result. */
25
25
  export declare const MCP_SERVER_NAME = "kiln";
26
- export declare const MCP_SERVER_VERSION = "1.2.0";
26
+ export declare const MCP_SERVER_VERSION = "1.2.1";
27
27
  /**
28
28
  * The spec's optional `instructions` field, returned by `initialize` and
29
29
  * `server/discover`.
@@ -17,7 +17,7 @@ import { type ViewRenderTimeoutContextProvider, type ViewRenderTimeoutResolver }
17
17
  import { ViewEvidenceHistoryStore } from "../views/evidence-history.js";
18
18
  import { type BackdropId } from "../views/background.js";
19
19
  import type { TextureUsage } from "../textures.js";
20
- export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
20
+ export { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
21
21
  export interface KilnToolDef {
22
22
  /** Shared action ownership and requirements; never hand-maintained by transports. */
23
23
  actionContract?: Readonly<Record<string, import("./operation-contract.js").KilnActionContract>>;
@@ -793,4 +793,3 @@ export declare function newestRevision(target: {
793
793
  export declare function buildProgramAssetDraft(code: string, context: KilnToolContext, backdrop?: BackdropId): Promise<Pick<import("../assets.js").AssetDraft, 'code' | 'glb' | 'preview' | 'previewInfo' | 'build'>>;
794
794
  /** All asset operation schemas live here, alongside the existing tool definitions. */
795
795
  export declare function createKilnAssetDefs(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef[];
796
- export {};
@@ -35,7 +35,8 @@ import { persistedPreviewFidelity } from "./preview-fidelity.js";
35
35
  // =============================================================================
36
36
  // Tool definition contract
37
37
  // =============================================================================
38
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
38
+ export { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
39
+ import { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
39
40
  function proceduralTextureMaterialContract(rendered, context) {
40
41
  const required = [...new Set(context.requiredProceduralTextureUsages ?? [])];
41
42
  if (required.length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Create and refine procedural 3D assets with JavaScript, image feedback, and reusable source revisions.",
5
5
  "license": "MIT",
6
6
  "author": "Matthew Kissinger",
package/plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kiln",
3
3
  "displayName": "Kiln",
4
4
  "description": "Build and revise 3D assets with your coding agent. Write JavaScript, inspect selected views, edit saved source revisions, and export GLB files locally.",
5
- "version": "1.2.0",
5
+ "version": "1.2.1",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/instruktlabs/kiln",
8
8
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiln-engine",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
5
5
  "author": {
6
6
  "name": "Instrukt Labs",
@@ -1,7 +1,7 @@
1
1
  # Kiln local plugin
2
2
 
3
- Plugin version 1.2.0, maintained by Instrukt Labs under the MIT license.
4
- This plugin pins engine 1.2.0. Registry setup requires that exact version
3
+ Plugin version 1.2.1, maintained by Instrukt Labs under the MIT license.
4
+ This plugin pins engine 1.2.1. Registry setup requires that exact version
5
5
  to be published. Use a published release tag for normal installation or a reviewed
6
6
  local archive for development qualification. The installation ID remains
7
7
  `kiln-engine@instruktlabs`.
@@ -31,7 +31,7 @@ codex plugin add kiln-engine@instruktlabs
31
31
  Restart or reload your coding agent's plugins as its installation message directs.
32
32
  Then ask it to set up a Kiln workspace. The maintained setup skill runs this
33
33
  plugin's `bin/kiln-setup-workspace.mjs` with Node, installs
34
- `@instruktlabs/kiln@1.2.0` outside the project and configures your chosen
34
+ `@instruktlabs/kiln@1.2.1` outside the project and configures your chosen
35
35
  workspace. The same initializer supports existing projects and new workspaces.
36
36
 
37
37
  The plugin registers setup only. The workspace supplies authoring skills and one
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "kind": "kiln-local-plugin",
4
- "pluginVersion": "1.2.0",
5
- "engineVersion": "1.2.0",
4
+ "pluginVersion": "1.2.1",
5
+ "engineVersion": "1.2.1",
6
6
  "executables": [
7
7
  "bin/kiln-setup-workspace.mjs"
8
8
  ],
9
9
  "files": {
10
10
  ".claude-plugin/icon.png": "sha256:b4b651c402793b7e27cab780f04b14f80daec6d0cd5b7e4e90313930581a54e9",
11
- ".claude-plugin/plugin.json": "sha256:616a6a61da072fcfdd4d0e1cf513e0e91bf1e56fb61b9b02d23e72fa291556bc",
11
+ ".claude-plugin/plugin.json": "sha256:bef717ea5e0c96411c2db93d54e298dc3798512df9909ba74c950a6c1c03f6cb",
12
12
  "bin/kiln-setup-workspace.mjs": "sha256:3d9a30236919a6ca91acb6b26f808cec442e6edc3dd88fe010645b010d76e838",
13
13
  "LICENSE": "sha256:cdba278ebf1d6de618869936226bf03be4887179c128efc7aaa13d22034826a5",
14
- "plugin.json": "sha256:3437107d46ad4fef4a710a8e821e56b43894cc1722504fe4e8ff0120fa6a8d8d",
14
+ "plugin.json": "sha256:9ef79b862a25ae3efe86c03a37e09cd622912b97a0165a08fdf55cff8b71a010",
15
15
  "PRIVACY.md": "sha256:5f58d981667e40441e21c2dec411f269a9a67a6141cec61976d27088fb12573b",
16
- "README.md": "sha256:0b82e6eb8b679f8b991c1a5567957574ed7b3341a87ef989403e5521f83ab50e",
17
- "runtime.json": "sha256:5ebac8387f4bedcc7cac02a36f73c5ffba969ce6c8c6e2b0e356ef569168d0a5",
16
+ "README.md": "sha256:029732d7bb9e04561105727f8e46118d3106f513b4575a8d74d57cbd1bb68e7a",
17
+ "runtime.json": "sha256:b6d078f1223cb5ed3a522b0ef5a3cb778e051afc0115237cfd9cfe04c1f42025",
18
18
  "skills/kiln-setup-workspace/references/plugin-install.md": "sha256:c338fb792e7fad0364c3e1a13c94c506afa1395489ec1b6e83b36d04728935fb",
19
19
  "skills/kiln-setup-workspace/references/renderer-setup.md": "sha256:b743b1b2827fa814385990e1e5b849391cb427413e9c016d273aa19cebd46031",
20
20
  "skills/kiln-setup-workspace/SKILL.md": "sha256:65ed926b3503a33f4fe793e84e4a045ce36e3967f0e6b340e456a15dd010ce54"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "kiln-engine",
4
- "version": "1.2.0",
4
+ "version": "1.2.1",
5
5
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
6
6
  "author": {
7
7
  "name": "Instrukt Labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "harnesses": [
5
5
  "claude",
6
6
  "codex",
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
9
+ export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v6.html';
@@ -27,7 +27,7 @@
27
27
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
28
28
  * or with any plugin manifest, which is what keeps the literal honest.
29
29
  */
30
- export const ENGINE_VERSION = '1.2.0';
30
+ export const ENGINE_VERSION = '1.2.1';
31
31
 
32
32
  /**
33
33
  * Where this engine was loaded from, as a `file://` URL of the package root.
@@ -1733,13 +1733,13 @@
1733
1733
  },
1734
1734
  "_meta": {
1735
1735
  "ui": {
1736
- "resourceUri": "ui://kiln/asset-v5.html",
1736
+ "resourceUri": "ui://kiln/asset-v6.html",
1737
1737
  "visibility": [
1738
1738
  "model"
1739
1739
  ]
1740
1740
  },
1741
- "openai/outputTemplate": "ui://kiln/asset-v5.html",
1742
- "ui/resourceUri": "ui://kiln/asset-v5.html"
1741
+ "openai/outputTemplate": "ui://kiln/asset-v6.html",
1742
+ "ui/resourceUri": "ui://kiln/asset-v6.html"
1743
1743
  },
1744
1744
  "outputSchema": {
1745
1745
  "type": "object",
@@ -2018,7 +2018,7 @@
2018
2018
  ],
2019
2019
  "resources": [
2020
2020
  {
2021
- "uri": "ui://kiln/asset-v5.html",
2021
+ "uri": "ui://kiln/asset-v6.html",
2022
2022
  "name": "kiln-asset-viewer",
2023
2023
  "mimeType": "text/html;profile=mcp-app",
2024
2024
  "description": "Interactive Kiln asset viewer and downloads",
@@ -94,7 +94,8 @@ import type { TextureUsage } from '../textures';
94
94
  // Tool definition contract
95
95
  // =============================================================================
96
96
 
97
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
97
+ export { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
98
+ import { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
98
99
  export interface KilnToolDef {
99
100
  /** Shared action ownership and requirements; never hand-maintained by transports. */
100
101
  actionContract?: Readonly<Record<string, import('./operation-contract').KilnActionContract>>;
@@ -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 = `3D preview unavailable: ${String(error)}`;
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;
@@ -0,0 +1,78 @@
1
+ import {
2
+ LoadingManager,
3
+ TextureLoader,
4
+ Texture,
5
+ type Object3D,
6
+ type Mesh,
7
+ type BufferGeometry,
8
+ type Material,
9
+ } from 'three';
10
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
11
+ import { validateAssetGlb } from '../assets';
12
+
13
+ export function disposeAssetModel(model: Object3D) {
14
+ const geometries = new Set<BufferGeometry>();
15
+ const materials = new Set<Material>();
16
+ const textures = new Set<Texture>();
17
+ model.traverse((object) => {
18
+ const mesh = object as Mesh;
19
+ if (!mesh.isMesh) return;
20
+ geometries.add(mesh.geometry);
21
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
22
+ materials.add(material);
23
+ for (const value of Object.values(material))
24
+ if (value instanceof Texture) textures.add(value);
25
+ }
26
+ });
27
+ for (const value of [...geometries, ...materials, ...textures]) value.dispose();
28
+ }
29
+
30
+ /**
31
+ * Parse self-contained GLBs under MCP Apps' img-src data: / connect-src none policy.
32
+ * ImageBitmapLoader fetches even blob/data URLs. Use the documented image handler
33
+ * instead, preserving GLTFLoader's sampler, UV, color-space and extension logic.
34
+ * Only the parser's private JSON copy changes; downloaded GLB bytes remain exact.
35
+ */
36
+ export async function loadAssetGlb(bytes: Uint8Array) {
37
+ validateAssetGlb(bytes);
38
+ const manager = new LoadingManager();
39
+ let imageFailed = false;
40
+ manager.onError = () => {
41
+ imageFailed = true;
42
+ };
43
+ manager.addHandler(/^data:image\//, new TextureLoader(manager));
44
+ manager.setURLModifier((url) => {
45
+ if (!url.startsWith('blob:') && !url.startsWith('data:'))
46
+ throw new Error('External model resources are not loaded');
47
+ return url;
48
+ });
49
+ const loader = new GLTFLoader(manager);
50
+ loader.register((parser) => ({
51
+ name: 'KILN_embedded_images',
52
+ async beforeRoot() {
53
+ for (const image of parser.json.images ?? []) {
54
+ if (image.bufferView === undefined) continue;
55
+ const buffer = new Uint8Array(await parser.getDependency('bufferView', image.bufferView));
56
+ let binary = '';
57
+ for (let i = 0; i < buffer.length; i += 8192)
58
+ binary += String.fromCharCode(...buffer.subarray(i, i + 8192));
59
+ if (!/^image\/(png|jpeg|webp|avif)$/.test(image.mimeType ?? ''))
60
+ throw new Error('Unsupported embedded image type');
61
+ image.uri = `data:${image.mimeType};base64,${btoa(binary)}`;
62
+ delete image.bufferView;
63
+ }
64
+ },
65
+ }));
66
+ const gltf = await loader.parseAsync(Uint8Array.from(bytes).buffer, '');
67
+ if (imageFailed) {
68
+ for (const scene of gltf.scenes) disposeAssetModel(scene);
69
+ throw new Error('An embedded texture could not be decoded.');
70
+ }
71
+ // GLTFLoader deliberately converts failed images to null maps. Never commit a
72
+ // material-degraded scene, including resources loaded later for detached LODs.
73
+ return Object.assign(gltf, {
74
+ assertTexturesLoaded() {
75
+ if (imageFailed) throw new Error('An embedded texture could not be decoded.');
76
+ },
77
+ });
78
+ }
@@ -1,9 +1,8 @@
1
1
  /// <reference lib="dom" />
2
2
  import * as THREE from 'three';
3
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
4
3
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5
4
  import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
6
- import { validateAssetGlb } from '../assets';
5
+ import { loadAssetGlb, disposeAssetModel as disposeModel } from './glb-loader';
7
6
  import { frameAssetBounds } from './framing';
8
7
  import {
9
8
  captureCameraView,
@@ -156,22 +155,6 @@ export function createAssetStage(container: HTMLElement) {
156
155
  ],
157
156
  };
158
157
  };
159
- const disposeModel = (model: THREE.Object3D) => {
160
- const geometries = new Set<THREE.BufferGeometry>();
161
- const materials = new Set<THREE.Material>();
162
- const textures = new Set<THREE.Texture>();
163
- model.traverse((object) => {
164
- const mesh = object as THREE.Mesh;
165
- if (!mesh.isMesh) return;
166
- geometries.add(mesh.geometry);
167
- for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
168
- materials.add(material);
169
- for (const value of Object.values(material))
170
- if (value instanceof THREE.Texture) textures.add(value);
171
- }
172
- });
173
- for (const value of [...geometries, ...materials, ...textures]) value.dispose();
174
- };
175
158
  const reset = () => {
176
159
  if (!root) return;
177
160
  const box = drawnBounds(root);
@@ -263,17 +246,17 @@ export function createAssetStage(container: HTMLElement) {
263
246
  const loadStarted = performance.now();
264
247
  const current = ++generation;
265
248
  cancelMeasurement('The artifact changed during measurement. Measure the new revision again.');
266
- validateAssetGlb(bytes);
267
- const manager = new THREE.LoadingManager();
268
- manager.setURLModifier((url) => {
269
- if (!url.startsWith('blob:') && !url.startsWith('data:'))
270
- throw new Error('External model resources are not loaded');
271
- return url;
272
- });
273
- const parsed = await new GLTFLoader(manager).parseAsync(Uint8Array.from(bytes).buffer, '');
249
+ const parsed = await loadAssetGlb(bytes);
274
250
  // three's loader draws every node; hide what KHR_node_visibility hides before counting.
275
251
  applyNodeVisibility(parsed, [parsed.scene]);
276
252
  const parsedLevels = await loadViewerLevels(parsed);
253
+ try {
254
+ parsed.assertTexturesLoaded();
255
+ } catch (error) {
256
+ disposeModel(parsed.scene);
257
+ for (const level of detachedLevels(parsedLevels)) disposeModel(level);
258
+ throw error;
259
+ }
277
260
  const gltf = acceptLoadedResource(
278
261
  parsed,
279
262
  () => !disposed && current === generation && options.isCurrent?.() !== false,