@compilr-dev/sdk 0.29.8 → 0.30.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.
@@ -23,7 +23,41 @@ import { validateControlManifest } from '../../canvas/validate.js';
23
23
  import { canvasMacro, infographicMacro, carouselMacro, boardMacro, canvasStylesMacro, } from '../../skills/canvas-macros.js';
24
24
  import { canvasExemplarInfographicMacro, canvasExemplarAppScreenMacro, } from '../../skills/canvas-exemplars.js';
25
25
  import { canvasIconsMacro } from '../../skills/canvas-icons.js';
26
- const CANVAS_TYPES = ['infographic', 'carousel', 'board'];
26
+ import { HTML_CANVAS_TYPES } from '../../canvas/types.js';
27
+ import { validateScene, normalizeScene, describeScene } from '../../canvas/scene.js';
28
+ import { sceneOutline, screenshotScene } from './scene-tools.js';
29
+ /**
30
+ * The HTML canvas types. `scene` is deliberately NOT here: a scene's content is JSON written
31
+ * through the scene writer, and canvas_write/canvas_edit on it is how a scene gets corrupted.
32
+ */
33
+ const CANVAS_TYPES = [...HTML_CANVAS_TYPES];
34
+ /** The refusal every HTML-content tool gives for a scene — names the tools that do work. */
35
+ function sceneRefusal(c) {
36
+ return (`Canvas ${String(c.id)} is a 3D scene; its content is JSON, not HTML. Edit it with ` +
37
+ 'scene_update_object / scene_add_object / scene_remove_object (read it first with scene_get).');
38
+ }
39
+ /** Scenes carry no images (P1); the asset tools say so and point at the scene tools. */
40
+ function sceneAssetRefusal(c) {
41
+ return (`Canvas ${String(c.id)} is a 3D scene; scenes have no images. Shape it with ` +
42
+ 'scene_add_object / scene_update_object instead.');
43
+ }
44
+ /** Static checks for scene JSON (canvas_validate on a scene) — the scene validator's errors. */
45
+ function sceneContentIssues(content) {
46
+ let raw;
47
+ try {
48
+ raw = JSON.parse(content);
49
+ }
50
+ catch (e) {
51
+ return [
52
+ {
53
+ level: 'error',
54
+ message: `Scene content is not valid JSON (${e instanceof Error ? e.message : String(e)}).`,
55
+ },
56
+ ];
57
+ }
58
+ const v = validateScene(raw);
59
+ return v.ok ? [] : v.errors.map((message) => ({ level: 'error', message }));
60
+ }
27
61
  /** Render content as numbered lines (1-based), optionally a slice. */
28
62
  function numberedLines(content, startLine, maxLines) {
29
63
  const lines = content.split('\n');
@@ -142,6 +176,9 @@ function offCanvasNodes(src, bounds) {
142
176
  }
143
177
  /** Run deterministic static checks on canvas content. No rendering. */
144
178
  export function runCanvasChecks(html, type, manifest) {
179
+ // A scene is JSON, not HTML: none of the HTML checks below apply.
180
+ if (type === 'scene')
181
+ return sceneContentIssues(html);
145
182
  const issues = [];
146
183
  const src = html.trim();
147
184
  if (!src) {
@@ -285,6 +322,10 @@ export function createCanvasTools(config, imageConfig) {
285
322
  },
286
323
  execute: async (input) => {
287
324
  try {
325
+ if (input.type === 'scene') {
326
+ return createErrorResult('canvas_write authors HTML canvases. 3D scenes are JSON — create one with scene_create, ' +
327
+ 'then build it with scene_add_object.');
328
+ }
288
329
  // Reject content that can't render in the sandbox (Mermaid/Markdown/
289
330
  // external-CDN) with a corrective message → the agent retries with
290
331
  // inline HTML/SVG instead of persisting a blank canvas.
@@ -298,6 +339,9 @@ export function createCanvasTools(config, imageConfig) {
298
339
  }
299
340
  }
300
341
  if (input.canvas_id !== undefined) {
342
+ const existing = await canvases.getById(input.canvas_id);
343
+ if (existing?.type === 'scene')
344
+ return createErrorResult(sceneRefusal(existing));
301
345
  const updated = await canvases.update(input.canvas_id, {
302
346
  title: input.title,
303
347
  content: input.html,
@@ -408,6 +452,20 @@ export function createCanvasTools(config, imageConfig) {
408
452
  const c = await canvases.getById(input.canvas_id);
409
453
  if (!c)
410
454
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
455
+ if (c.type === 'scene') {
456
+ // A scene is JSON — an outline of its objects, and the tool that reads it whole.
457
+ let body;
458
+ try {
459
+ const v = validateScene(JSON.parse(c.content));
460
+ body = v.ok
461
+ ? `${describeScene(v.scene)}\n\n--- objects (id · type · name) ---\n${sceneOutline(normalizeScene(v.scene))}`
462
+ : `The stored scene is invalid (${v.errors[0] ?? 'unknown error'}).`;
463
+ }
464
+ catch {
465
+ body = 'The stored scene is not valid JSON.';
466
+ }
467
+ return createSuccessResult(`Canvas "${c.title}" (scene)\nID: ${String(c.id)} | ${body}\n\nRead the full scene with scene_get.`);
468
+ }
411
469
  const totalLines = c.content.split('\n').length;
412
470
  const header = `Canvas "${c.title}" (${c.type})\n` +
413
471
  `ID: ${String(c.id)} | Controls: ${String(c.controls.controls.length)} | Lines: ${String(totalLines)}\n\n`;
@@ -474,6 +532,9 @@ export function createCanvasTools(config, imageConfig) {
474
532
  const c = await canvases.getById(input.canvas_id);
475
533
  if (!c)
476
534
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
535
+ // str_replace on scene JSON is exactly how a scene gets corrupted.
536
+ if (c.type === 'scene')
537
+ return createErrorResult(sceneRefusal(c));
477
538
  let next;
478
539
  let summary;
479
540
  if (input.operation === 'str_replace') {
@@ -559,8 +620,8 @@ export function createCanvasTools(config, imageConfig) {
559
620
  },
560
621
  type: {
561
622
  type: 'string',
562
- enum: CANVAS_TYPES,
563
- description: 'Canvas type (required when passing html; defaults to infographic).',
623
+ enum: [...CANVAS_TYPES, 'scene'],
624
+ description: 'Canvas type (required when passing html; defaults to infographic). For "scene", pass the scene JSON as html.',
564
625
  },
565
626
  },
566
627
  required: [],
@@ -649,13 +710,41 @@ export function createCanvasTools(config, imageConfig) {
649
710
  execute: async (input) => {
650
711
  try {
651
712
  const renderer = ctx.canvasRenderer;
713
+ // A scene goes to the 3D renderer — either tool works on a scene (3d-canvas-spec §3.1).
714
+ const maybeScene = await canvases.getById(input.canvas_id);
715
+ if (maybeScene?.type === 'scene') {
716
+ let scene;
717
+ try {
718
+ const v = validateScene(JSON.parse(maybeScene.content));
719
+ if (!v.ok)
720
+ return createErrorResult(`${v.errors[0] ?? 'Invalid scene.'} Read it with scene_get.`);
721
+ scene = normalizeScene(v.scene);
722
+ }
723
+ catch {
724
+ return createErrorResult('The stored scene is not valid JSON. Read it with scene_get.');
725
+ }
726
+ const shot = await screenshotScene(ctx, {
727
+ canvasId: maybeScene.id,
728
+ title: maybeScene.title,
729
+ scene,
730
+ });
731
+ if (!shot.available)
732
+ return createSuccessResult(shot.text);
733
+ return {
734
+ success: true,
735
+ result: shot.text,
736
+ imageBlocks: [{ ...shot.image, filename: `scene-${String(maybeScene.id)}.png` }],
737
+ };
738
+ }
652
739
  if (!renderer) {
653
740
  return createSuccessResult('Canvas rendering is not available in this environment — a visual self-review can’t be produced here. ' +
654
741
  'Rely on canvas_validate (static checks) instead.');
655
742
  }
656
- const c = await canvases.getById(input.canvas_id);
743
+ const c = maybeScene;
657
744
  if (!c)
658
745
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
746
+ // Scenes returned above; what is left is an HTML canvas.
747
+ const htmlType = c.type;
659
748
  /*
660
749
  ⚠️ Resolve assets HERE, not in the host renderer. This tool has the canvas id and
661
750
  the repository; `renderToImage` receives only html, so pushing substitution into
@@ -666,7 +755,7 @@ export function createCanvasTools(config, imageConfig) {
666
755
  const shotAssets = canvases.listAssets ? await canvases.listAssets(c.id) : [];
667
756
  const img = await renderer.renderToImage({
668
757
  content: resolveCanvasAssets(c.content, shotAssets),
669
- type: c.type,
758
+ type: htmlType,
670
759
  });
671
760
  return {
672
761
  success: true,
@@ -812,6 +901,8 @@ export function createCanvasTools(config, imageConfig) {
812
901
  const canvas = await canvases.getById(input.canvas_id);
813
902
  if (!canvas)
814
903
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
904
+ if (canvas.type === 'scene')
905
+ return createErrorResult(sceneAssetRefusal(canvas));
815
906
  const filePath = pathNode.isAbsolute(input.path)
816
907
  ? input.path
817
908
  : pathNode.resolve(resolveCwd(config.cwd), input.path);
@@ -894,6 +985,8 @@ export function createCanvasTools(config, imageConfig) {
894
985
  const canvas = await canvases.getById(input.canvas_id);
895
986
  if (!canvas)
896
987
  return createErrorResult(`Canvas ${String(input.canvas_id)} not found.`);
988
+ if (canvas.type === 'scene')
989
+ return createErrorResult(sceneAssetRefusal(canvas));
897
990
  const assets = await canvases.listAssets(input.canvas_id);
898
991
  if (assets.length === 0) {
899
992
  return createSuccessResult('No images on this canvas. Add one with canvas_asset_add.');
@@ -925,6 +1018,9 @@ export function createCanvasTools(config, imageConfig) {
925
1018
  if (!canvases.deleteAsset) {
926
1019
  return createErrorResult('This host does not support canvas assets.');
927
1020
  }
1021
+ const target = await canvases.getById(input.canvas_id);
1022
+ if (target?.type === 'scene')
1023
+ return createErrorResult(sceneAssetRefusal(target));
928
1024
  const gone = await canvases.deleteAsset(input.canvas_id, input.ref.toLowerCase());
929
1025
  return gone
930
1026
  ? createSuccessResult(`Removed "${input.ref}".`)
@@ -32,5 +32,6 @@ export { createAnchorTools } from './anchor-tools.js';
32
32
  export { createArtifactTools } from './artifact-tools.js';
33
33
  export { createEpisodeTools } from './episode-tools.js';
34
34
  export { createCanvasTools } from './canvas-tools.js';
35
+ export { createSceneTools, SCENE_TOOL_NAMES, sceneOutline } from './scene-tools.js';
35
36
  export { createImageTools } from './image-tools.js';
36
37
  export type { ImageToolsConfig, ImageResizer } from './image-tools.js';
@@ -17,6 +17,8 @@ import { createAnchorTools } from './anchor-tools.js';
17
17
  import { createArtifactTools } from './artifact-tools.js';
18
18
  import { createEpisodeTools } from './episode-tools.js';
19
19
  import { createCanvasTools } from './canvas-tools.js';
20
+ import { createSceneTools } from './scene-tools.js';
21
+ import { createSceneWriter } from '../scene-writer.js';
20
22
  import { createImageTools } from './image-tools.js';
21
23
  /**
22
24
  * Create all platform tools operating against the given context.
@@ -63,8 +65,21 @@ export function createPlatformTools(config, options) {
63
65
  tools.push(...createEpisodeTools(config));
64
66
  }
65
67
  if (config.context.canvases) {
66
- // The resizer is shared with view_image — canvas_asset_add resizes to display size.
67
- tools.push(...createCanvasTools(config, imageConfig));
68
+ const surfaces = config.canvasSurfaces ?? {};
69
+ if (surfaces.html !== false) {
70
+ // The resizer is shared with view_image — canvas_asset_add resizes to display size.
71
+ tools.push(...createCanvasTools(config, imageConfig));
72
+ }
73
+ if (surfaces.scene !== false) {
74
+ /*
75
+ One writer for the scene tools — the host's own when given (Desktop's inspector must
76
+ share it), else one built here, ONCE, so every scene tool shares its lock and journal.
77
+ ⚠️ Passed alongside, never by spreading a new context: hosts mutate
78
+ `context.currentProjectId` after the tools are built, and a copy would freeze it.
79
+ */
80
+ const sceneWriter = config.context.sceneWriter ?? createSceneWriter(config.context.canvases);
81
+ tools.push(...createSceneTools(config, { writer: sceneWriter }));
82
+ }
68
83
  }
69
84
  return tools;
70
85
  }
@@ -78,4 +93,5 @@ export { createAnchorTools } from './anchor-tools.js';
78
93
  export { createArtifactTools } from './artifact-tools.js';
79
94
  export { createEpisodeTools } from './episode-tools.js';
80
95
  export { createCanvasTools } from './canvas-tools.js';
96
+ export { createSceneTools, SCENE_TOOL_NAMES, sceneOutline } from './scene-tools.js';
81
97
  export { createImageTools } from './image-tools.js';
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Scene Tools — agents build 3D scenes as validated JSON (3d-canvas-spec §6).
3
+ *
4
+ * 7 tools: scene_create, scene_get, scene_add_object, scene_update_object,
5
+ * scene_remove_object, scene_set_camera, scene_screenshot.
6
+ *
7
+ * Every edit goes through the scene writer (`platform/scene-writer.ts`) — the same instance the
8
+ * host's inspector uses — so user and agent edits serialise and the agent is TOLD about the
9
+ * user's changes: a successful result starts with "The user edited this scene since your last
10
+ * call: …" when there are any.
11
+ *
12
+ * Names carry the `scene_` prefix: generic names (`add_object`) collide with MCP servers, and
13
+ * hosts refresh views on tool-name prefixes.
14
+ */
15
+ import type { PlatformContext, PlatformToolsConfig } from '../context.js';
16
+ import { type ISceneWriter } from '../scene-writer.js';
17
+ import { type SceneFile } from '../../canvas/scene.js';
18
+ export declare const SCENE_TOOL_NAMES: readonly ["scene_create", "scene_get", "scene_add_object", "scene_update_object", "scene_remove_object", "scene_set_camera", "scene_screenshot"];
19
+ /** "sofa · box · Sofa" rows, indented under their group parent. */
20
+ export declare function sceneOutline(scene: SceneFile): string;
21
+ /**
22
+ * Render a scene through the host's `renderSceneToImage` — shared by scene_screenshot and
23
+ * canvas_screenshot (either tool works on a scene).
24
+ */
25
+ export declare function screenshotScene(ctx: PlatformContext, args: {
26
+ canvasId: number;
27
+ title: string;
28
+ scene: SceneFile;
29
+ width?: number;
30
+ height?: number;
31
+ }): Promise<{
32
+ available: false;
33
+ text: string;
34
+ } | {
35
+ available: true;
36
+ text: string;
37
+ image: {
38
+ data: string;
39
+ mediaType: string;
40
+ width?: number;
41
+ height?: number;
42
+ };
43
+ }>;
44
+ export declare function createSceneTools(config: PlatformToolsConfig, options?: {
45
+ writer?: ISceneWriter;
46
+ }): (import("@compilr-dev/agents").Tool<{
47
+ title: string;
48
+ scene?: unknown;
49
+ project_id?: number;
50
+ }> | import("@compilr-dev/agents").Tool<Record<string, unknown> & {
51
+ canvas_id: number;
52
+ }> | import("@compilr-dev/agents").Tool<{
53
+ canvas_id: number;
54
+ id: string;
55
+ patch: Record<string, unknown>;
56
+ }> | import("@compilr-dev/agents").Tool<{
57
+ canvas_id: number;
58
+ position?: unknown;
59
+ target?: unknown;
60
+ fit?: boolean;
61
+ }> | import("@compilr-dev/agents").Tool<{
62
+ canvas_id: number;
63
+ width?: number;
64
+ height?: number;
65
+ }>)[];