@pascal-app/mcp 1.0.3 → 1.0.4

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 (193) hide show
  1. package/README.md +47 -26
  2. package/dist/bin/pascal-mcp.js +8 -2
  3. package/dist/bridge/scene-bridge.d.ts +39 -6
  4. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  5. package/dist/bridge/scene-bridge.js +225 -45
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/operations/scene-operations.d.ts +23 -3
  10. package/dist/operations/scene-operations.d.ts.map +1 -1
  11. package/dist/operations/scene-operations.js +31 -1
  12. package/dist/prompts/from-brief.js +1 -1
  13. package/dist/prompts/iterate-on-feedback.js +1 -1
  14. package/dist/prompts/scene-guidance.js +3 -3
  15. package/dist/resources/agent-guide.d.ts.map +1 -1
  16. package/dist/resources/agent-guide.js +22 -1
  17. package/dist/resources/catalog-items.d.ts +4 -5
  18. package/dist/resources/catalog-items.d.ts.map +1 -1
  19. package/dist/resources/catalog-items.js +7 -9
  20. package/dist/resources/design-schema.d.ts +9 -0
  21. package/dist/resources/design-schema.d.ts.map +1 -0
  22. package/dist/resources/design-schema.js +21 -0
  23. package/dist/resources/index.d.ts +3 -1
  24. package/dist/resources/index.d.ts.map +1 -1
  25. package/dist/resources/index.js +5 -2
  26. package/dist/server.d.ts +19 -0
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +14 -6
  29. package/dist/storage/disk-artifact-store.d.ts +9 -0
  30. package/dist/storage/disk-artifact-store.d.ts.map +1 -0
  31. package/dist/storage/disk-artifact-store.js +46 -0
  32. package/dist/storage/index.d.ts +1 -0
  33. package/dist/storage/index.d.ts.map +1 -1
  34. package/dist/storage/index.js +1 -0
  35. package/dist/storage/sqlite-scene-store.d.ts +2 -2
  36. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  37. package/dist/storage/sqlite-scene-store.js +4 -3
  38. package/dist/storage/types.d.ts +32 -3
  39. package/dist/storage/types.d.ts.map +1 -1
  40. package/dist/storage/types.js +12 -0
  41. package/dist/tools/add-column.d.ts +5 -0
  42. package/dist/tools/add-column.d.ts.map +1 -0
  43. package/dist/tools/add-column.js +58 -0
  44. package/dist/tools/add-object.d.ts +60 -0
  45. package/dist/tools/add-object.d.ts.map +1 -0
  46. package/dist/tools/add-object.js +156 -0
  47. package/dist/tools/apply-patch.d.ts +3 -0
  48. package/dist/tools/apply-patch.d.ts.map +1 -1
  49. package/dist/tools/apply-patch.js +28 -2
  50. package/dist/tools/asset-catalog.d.ts +9 -5
  51. package/dist/tools/asset-catalog.d.ts.map +1 -1
  52. package/dist/tools/asset-catalog.js +10 -12
  53. package/dist/tools/check-collisions.d.ts +1 -1
  54. package/dist/tools/check-collisions.d.ts.map +1 -1
  55. package/dist/tools/check-collisions.js +1 -2
  56. package/dist/tools/clear-scene.d.ts +9 -0
  57. package/dist/tools/clear-scene.d.ts.map +1 -0
  58. package/dist/tools/clear-scene.js +59 -0
  59. package/dist/tools/collections.d.ts +4 -0
  60. package/dist/tools/collections.d.ts.map +1 -0
  61. package/dist/tools/collections.js +52 -0
  62. package/dist/tools/construction-tools.d.ts +8 -41
  63. package/dist/tools/construction-tools.d.ts.map +1 -1
  64. package/dist/tools/construction-tools.js +120 -230
  65. package/dist/tools/create-unit.d.ts +2 -0
  66. package/dist/tools/create-unit.d.ts.map +1 -1
  67. package/dist/tools/describe-node.d.ts.map +1 -1
  68. package/dist/tools/describe-node.js +8 -6
  69. package/dist/tools/errors.d.ts +11 -0
  70. package/dist/tools/errors.d.ts.map +1 -1
  71. package/dist/tools/errors.js +10 -0
  72. package/dist/tools/find-nodes.d.ts +3 -19
  73. package/dist/tools/find-nodes.d.ts.map +1 -1
  74. package/dist/tools/find-nodes.js +150 -53
  75. package/dist/tools/geometry-script-worker.d.ts +2 -0
  76. package/dist/tools/geometry-script-worker.d.ts.map +1 -0
  77. package/dist/tools/geometry-script-worker.js +21 -0
  78. package/dist/tools/get-scene.d.ts +1 -0
  79. package/dist/tools/get-scene.d.ts.map +1 -1
  80. package/dist/tools/get-scene.js +3 -1
  81. package/dist/tools/honest-patch-guard.d.ts +2 -0
  82. package/dist/tools/honest-patch-guard.d.ts.map +1 -0
  83. package/dist/tools/honest-patch-guard.js +32 -0
  84. package/dist/tools/hosted-services.d.ts +4 -0
  85. package/dist/tools/hosted-services.d.ts.map +1 -0
  86. package/dist/tools/hosted-services.js +30 -0
  87. package/dist/tools/index.d.ts +10 -1
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +17 -26
  90. package/dist/tools/live-sync.d.ts +24 -5
  91. package/dist/tools/live-sync.d.ts.map +1 -1
  92. package/dist/tools/live-sync.js +47 -26
  93. package/dist/tools/local-geometry-scripts.d.ts +8 -0
  94. package/dist/tools/local-geometry-scripts.d.ts.map +1 -0
  95. package/dist/tools/local-geometry-scripts.js +67 -0
  96. package/dist/tools/measure.d.ts +6 -0
  97. package/dist/tools/measure.d.ts.map +1 -1
  98. package/dist/tools/measure.js +33 -65
  99. package/dist/tools/measurement.d.ts +1 -36
  100. package/dist/tools/measurement.d.ts.map +1 -1
  101. package/dist/tools/measurement.js +2 -94
  102. package/dist/tools/node-world-point.d.ts +37 -0
  103. package/dist/tools/node-world-point.d.ts.map +1 -0
  104. package/dist/tools/node-world-point.js +457 -0
  105. package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
  106. package/dist/tools/normalize-schema-dialect.js +20 -0
  107. package/dist/tools/patch-guards.d.ts +58 -0
  108. package/dist/tools/patch-guards.d.ts.map +1 -0
  109. package/dist/tools/patch-guards.js +253 -0
  110. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  111. package/dist/tools/photo-to-scene/photo-to-scene.js +3 -2
  112. package/dist/tools/place-design.d.ts +34 -0
  113. package/dist/tools/place-design.d.ts.map +1 -0
  114. package/dist/tools/place-design.js +91 -0
  115. package/dist/tools/redo.d.ts +2 -0
  116. package/dist/tools/redo.d.ts.map +1 -1
  117. package/dist/tools/redo.js +2 -2
  118. package/dist/tools/room-tools.d.ts +50 -113
  119. package/dist/tools/room-tools.d.ts.map +1 -1
  120. package/dist/tools/room-tools.js +129 -565
  121. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  122. package/dist/tools/scene-lifecycle/create-project.js +26 -1
  123. package/dist/tools/scene-lifecycle/delete-scene.d.ts +2 -0
  124. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  125. package/dist/tools/scene-lifecycle/delete-scene.js +4 -3
  126. package/dist/tools/scene-lifecycle/save-scene.d.ts +1 -0
  127. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  128. package/dist/tools/scene-lifecycle/save-scene.js +46 -6
  129. package/dist/tools/scene-lifecycle/test-utils.d.ts +2 -2
  130. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  131. package/dist/tools/scene-lifecycle/test-utils.js +2 -1
  132. package/dist/tools/set-unit-members.d.ts +2 -0
  133. package/dist/tools/set-unit-members.d.ts.map +1 -1
  134. package/dist/tools/set-zone.d.ts +2 -0
  135. package/dist/tools/set-zone.d.ts.map +1 -1
  136. package/dist/tools/set-zone.js +2 -2
  137. package/dist/tools/shared-tools.d.ts +30 -0
  138. package/dist/tools/shared-tools.d.ts.map +1 -0
  139. package/dist/tools/shared-tools.js +361 -0
  140. package/dist/tools/structure-tools.d.ts +57 -0
  141. package/dist/tools/structure-tools.d.ts.map +1 -0
  142. package/dist/tools/structure-tools.js +57 -0
  143. package/dist/tools/undo.d.ts +2 -0
  144. package/dist/tools/undo.d.ts.map +1 -1
  145. package/dist/tools/undo.js +2 -2
  146. package/dist/tools/validate-design.d.ts +136 -0
  147. package/dist/tools/validate-design.d.ts.map +1 -0
  148. package/dist/tools/validate-design.js +28 -0
  149. package/dist/tools/variants/mutations.js +1 -1
  150. package/dist/tools/view-scene.d.ts +38 -0
  151. package/dist/tools/view-scene.d.ts.map +1 -0
  152. package/dist/tools/view-scene.js +63 -0
  153. package/dist/version.d.ts +5 -0
  154. package/dist/version.d.ts.map +1 -1
  155. package/dist/version.js +17 -3
  156. package/examples/embed-in-agent.ts +1 -1
  157. package/examples/generate-apartment.md +9 -11
  158. package/examples/photo-to-scene.md +1 -1
  159. package/examples/renovate-from-photos.md +3 -4
  160. package/package.json +10 -4
  161. package/dist/tools/create-level.d.ts +0 -21
  162. package/dist/tools/create-level.d.ts.map +0 -1
  163. package/dist/tools/create-level.js +0 -63
  164. package/dist/tools/create-wall.d.ts +0 -22
  165. package/dist/tools/create-wall.d.ts.map +0 -1
  166. package/dist/tools/create-wall.js +0 -57
  167. package/dist/tools/cut-opening.d.ts +0 -25
  168. package/dist/tools/cut-opening.d.ts.map +0 -1
  169. package/dist/tools/cut-opening.js +0 -61
  170. package/dist/tools/delete-node.d.ts +0 -19
  171. package/dist/tools/delete-node.d.ts.map +0 -1
  172. package/dist/tools/delete-node.js +0 -40
  173. package/dist/tools/door-clearance.d.ts +0 -135
  174. package/dist/tools/door-clearance.d.ts.map +0 -1
  175. package/dist/tools/door-clearance.js +0 -334
  176. package/dist/tools/duplicate-level.d.ts +0 -19
  177. package/dist/tools/duplicate-level.d.ts.map +0 -1
  178. package/dist/tools/duplicate-level.js +0 -60
  179. package/dist/tools/geometry.d.ts +0 -24
  180. package/dist/tools/geometry.d.ts.map +0 -1
  181. package/dist/tools/geometry.js +0 -94
  182. package/dist/tools/get-node.d.ts +0 -11
  183. package/dist/tools/get-node.d.ts.map +0 -1
  184. package/dist/tools/get-node.js +0 -29
  185. package/dist/tools/layout-clearance.d.ts +0 -105
  186. package/dist/tools/layout-clearance.d.ts.map +0 -1
  187. package/dist/tools/layout-clearance.js +0 -190
  188. package/dist/tools/place-item.d.ts +0 -22
  189. package/dist/tools/place-item.d.ts.map +0 -1
  190. package/dist/tools/place-item.js +0 -89
  191. package/dist/tools/scene-query.d.ts +0 -62
  192. package/dist/tools/scene-query.d.ts.map +0 -1
  193. package/dist/tools/scene-query.js +0 -670
@@ -1,24 +1,15 @@
1
- import { syncAutoStairOpenings } from '@pascal-app/core/stair-openings';
1
+ import { refuse, writeTarget } from '@pascal-app/core/agent-tools';
2
2
  import { z } from 'zod';
3
- import { SceneVersionConflictError } from '../storage/types.js';
4
- import { ErrorCode, throwMcpError } from './errors.js';
5
- export function syncDerivedStairOpenings(operations) {
6
- const updates = syncAutoStairOpenings(operations.getNodes());
7
- if (updates.length === 0)
8
- return 0;
9
- operations.applyPatch(updates.map((update) => ({
10
- op: 'update',
11
- id: update.id,
12
- data: update.data,
13
- })));
14
- return updates.length;
15
- }
3
+ import { SceneVersionConflictError, SceneWipeBlockedError } from '../storage/types.js';
4
+ import { ErrorCode, McpError, throwMcpError } from './errors.js';
16
5
  /**
17
6
  * Output-schema fragment for every tool that mutates the scene. Spread into
18
7
  * the tool's `outputSchema` so `persistencePayload` fields survive the SDK's
19
8
  * structured-content validation.
20
9
  */
21
10
  export const liveSyncOutput = {
11
+ project: z.string().nullable().optional(),
12
+ unsaved: z.string().optional(),
22
13
  persistence: z
23
14
  .object({
24
15
  status: z.enum(['unbound', 'events_unsupported']),
@@ -31,13 +22,29 @@ const LIVE_SYNC_WARNINGS = {
31
22
  events_unsupported: 'The change was applied to the in-memory session only: the attached scene store does not support live scene events, so nothing was persisted.',
32
23
  };
33
24
  /**
34
- * Payload fragment matching `liveSyncOutput`: empty after a successful
35
- * publish, a `persistence` warning when the mutation stayed in-memory.
25
+ * Payload fragment matching `liveSyncOutput`: the project the write reached, or `null` and a note
26
+ * when none is bound; a `persistence` warning when the mutation stayed in-memory.
27
+ */
28
+ export function persistencePayload({ status, project }) {
29
+ return {
30
+ ...writeTarget(project),
31
+ ...(status === 'published'
32
+ ? {}
33
+ : { persistence: { status, warning: LIVE_SYNC_WARNINGS[status] } }),
34
+ };
35
+ }
36
+ /** Where the session's writes go, for a call that publishes nothing (an undo with nothing to undo). */
37
+ export function currentLiveSync(operations) {
38
+ const project = operations.getActiveScene()?.projectId ?? null;
39
+ return { status: project ? 'published' : 'unbound', project };
40
+ }
41
+ const LIVE_SYNC_VERSION_CONFLICT = 'live_sync_version_conflict';
42
+ /**
43
+ * Whether a tool call failed because the stored scene changed after the session
44
+ * loaded it. The store refuses before writing, so nothing from the call persisted.
36
45
  */
37
- export function persistencePayload(status) {
38
- if (status === 'published')
39
- return {};
40
- return { persistence: { status, warning: LIVE_SYNC_WARNINGS[status] } };
46
+ export function isLiveSyncVersionConflict(error) {
47
+ return error instanceof McpError && error.message.endsWith(LIVE_SYNC_VERSION_CONFLICT);
41
48
  }
42
49
  /**
43
50
  * Persist the bridge's current graph to the active scene and append a live
@@ -46,13 +53,15 @@ export function persistencePayload(status) {
46
53
  * append scene events; callers surface that through `persistencePayload` so
47
54
  * the skip is never silent (#725).
48
55
  */
49
- export async function publishLiveSceneSnapshot(operations, kind) {
50
- syncDerivedStairOpenings(operations);
56
+ export async function publishLiveSceneSnapshot(operations, kind,
57
+ /** `allowSceneWipe`: the write empties the project on purpose (clear_scene). */
58
+ options = {}) {
51
59
  const active = operations.getActiveScene();
52
60
  if (!active)
53
- return 'unbound';
61
+ return { status: 'unbound', project: null };
62
+ const project = active.projectId ?? active.id;
54
63
  if (!operations.canAppendSceneEvents)
55
- return 'events_unsupported';
64
+ return { status: 'events_unsupported', project };
56
65
  const graph = operations.exportSceneGraph();
57
66
  try {
58
67
  const meta = await operations.saveScene({
@@ -63,9 +72,11 @@ export async function publishLiveSceneSnapshot(operations, kind) {
63
72
  thumbnailUrl: active.thumbnailUrl,
64
73
  graph,
65
74
  expectedVersion: active.version,
75
+ ...(active.graphHash !== undefined ? { expectedGraphHash: active.graphHash } : {}),
66
76
  saveMode: 'draft',
67
77
  publish: false,
68
78
  operation: kind,
79
+ ...(options.allowSceneWipe ? { allowSceneWipe: true } : {}),
69
80
  });
70
81
  operations.setActiveScene(meta);
71
82
  await operations.appendSceneEvent({
@@ -76,8 +87,18 @@ export async function publishLiveSceneSnapshot(operations, kind) {
76
87
  });
77
88
  }
78
89
  catch (error) {
90
+ if (error instanceof SceneWipeBlockedError) {
91
+ // The store kept what it held; the session goes back to it, so the agent's next write builds
92
+ // on the project as stored rather than on the refused one (deleting the only room, say).
93
+ const stored = await operations.loadStoredScene(active.id).catch(() => null);
94
+ if (stored)
95
+ operations.loadJSON(stored.graph);
96
+ refuse('scene_wipe_blocked', stored
97
+ ? 'This write would leave the project empty, so it was blocked and nothing changed. To empty the project on purpose, call clear_scene. To remove only part of it, such as its only room, build what replaces it first, then remove it.'
98
+ : 'This write would leave the project empty, so it was blocked and not saved. Call load_scene before writing again. To empty the project on purpose, call clear_scene.', { sceneId: active.id, mutationApplied: false, sessionRestored: !!stored });
99
+ }
79
100
  if (error instanceof SceneVersionConflictError) {
80
- throwMcpError(ErrorCode.InvalidRequest, 'live_sync_version_conflict', {
101
+ throwMcpError(ErrorCode.InvalidRequest, LIVE_SYNC_VERSION_CONFLICT, {
81
102
  sceneId: active.id,
82
103
  expectedVersion: active.version,
83
104
  });
@@ -85,7 +106,7 @@ export async function publishLiveSceneSnapshot(operations, kind) {
85
106
  const message = error instanceof Error ? error.message : String(error);
86
107
  throwMcpError(ErrorCode.InternalError, `live_sync_failed: ${message}`);
87
108
  }
88
- return 'published';
109
+ return { status: 'published', project };
89
110
  }
90
111
  export async function appendLiveSceneEvent(operations, sceneId, version, kind, graph) {
91
112
  if (!operations.canAppendSceneEvents)
@@ -0,0 +1,8 @@
1
+ import type { GeometryScriptHost } from './add-object.js';
2
+ /**
3
+ * Local compile is opt-in: a separate process is not an untrusted-code sandbox.
4
+ * three.js modules need PASCAL_SERVER_SCRIPT_COMPILE=1; bpy modules run in the
5
+ * Blender binary PASCAL_BLENDER names.
6
+ */
7
+ export declare function localGeometryScripts(projectPath: string): GeometryScriptHost;
8
+ //# sourceMappingURL=local-geometry-scripts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-geometry-scripts.d.ts","sourceRoot":"","sources":["../../src/tools/local-geometry-scripts.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAEtD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,MAAM,GAAG,kBAAkB,CA4D5E"}
@@ -0,0 +1,67 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { compileBlenderBuild } from '@pascal-app/geometry-script/blender';
4
+ import { runBlenderLocally } from '@pascal-app/geometry-script/blender-local';
5
+ import { DiskArtifactStore } from '../storage/disk-artifact-store.js';
6
+ /**
7
+ * Local compile is opt-in: a separate process is not an untrusted-code sandbox.
8
+ * three.js modules need PASCAL_SERVER_SCRIPT_COMPILE=1; bpy modules run in the
9
+ * Blender binary PASCAL_BLENDER names.
10
+ */
11
+ export function localGeometryScripts(projectPath) {
12
+ const artifacts = new DiskArtifactStore(projectPath);
13
+ return {
14
+ compile: async (input) => {
15
+ if (input.language === 'bpy') {
16
+ const blender = process.env.PASCAL_BLENDER;
17
+ if (!blender)
18
+ throw new Error('bpy scripts need PASCAL_BLENDER, the path to a Blender binary');
19
+ const run = await runBlenderLocally({ blender, code: input.code, params: input.params });
20
+ const { glb, ...compiled } = await compileBlenderBuild({ ...input, run });
21
+ return { ...compiled, glb: new Uint8Array(glb) };
22
+ }
23
+ if (process.env.PASCAL_SERVER_SCRIPT_COMPILE !== '1') {
24
+ throw new Error('Local script compilation requires PASCAL_SERVER_SCRIPT_COMPILE=1');
25
+ }
26
+ return new Promise((resolve, reject) => {
27
+ const child = spawn(process.execPath, [fileURLToPath(new URL('./geometry-script-worker.js', import.meta.url))], { env: {}, stdio: ['pipe', 'pipe', 'pipe'] });
28
+ const chunks = [];
29
+ let stderr = '';
30
+ const timer = setTimeout(() => {
31
+ child.kill('SIGKILL');
32
+ reject(new Error('Geometry script compilation timed out'));
33
+ }, 20_000);
34
+ child.stdout.on('data', (chunk) => chunks.push(chunk));
35
+ child.stderr.on('data', (chunk) => {
36
+ stderr = (stderr + chunk.toString()).slice(-2000);
37
+ });
38
+ child.stdin.on('error', reject);
39
+ child.on('error', (error) => {
40
+ clearTimeout(timer);
41
+ reject(error);
42
+ });
43
+ child.on('close', () => {
44
+ clearTimeout(timer);
45
+ try {
46
+ const message = JSON.parse(Buffer.concat(chunks).toString());
47
+ if (!message.ok)
48
+ return reject(new Error(message.error));
49
+ resolve({
50
+ ...message.compiled,
51
+ glb: new Uint8Array(Buffer.from(message.glb, 'base64')),
52
+ });
53
+ }
54
+ catch {
55
+ reject(new Error(`Geometry script compilation failed: ${stderr}`));
56
+ }
57
+ });
58
+ child.stdin.end(JSON.stringify(input));
59
+ });
60
+ },
61
+ storeArtifact: async ({ sha256, bytes }) => {
62
+ await artifacts.put(sha256, bytes);
63
+ return sha256;
64
+ },
65
+ readArtifact: ({ sha256 }) => artifacts.read(sha256),
66
+ };
67
+ }
@@ -7,6 +7,12 @@ export declare const measureInput: {
7
7
  };
8
8
  export declare const measureOutput: {
9
9
  distanceMeters: z.ZodNumber;
10
+ fromPoint: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
11
+ toPoint: z.ZodOptional<z.ZodArray<z.ZodNumber>>;
12
+ approximate: z.ZodOptional<z.ZodArray<z.ZodObject<{
13
+ id: z.ZodString;
14
+ reason: z.ZodString;
15
+ }, z.core.$strip>>>;
10
16
  areaSqMeters: z.ZodOptional<z.ZodNumber>;
11
17
  units: z.ZodLiteral<"meters">;
12
18
  areaUnits: z.ZodOptional<z.ZodLiteral<"square_meters">>;
@@ -1 +1 @@
1
- {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../src/tools/measure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAKpD,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAED,eAAO,MAAM,aAAa;;;;;CAKzB,CAAA;AAsED,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CAyEhF"}
1
+ {"version":3,"file":"measure.d.ts","sourceRoot":"","sources":["../../src/tools/measure.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAExE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAMpD,eAAO,MAAM,YAAY;;;CAGxB,CAAA;AAED,eAAO,MAAM,aAAa;;;;;;;;;;;CAQzB,CAAA;AAeD,wBAAgB,eAAe,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,eAAe,GAAG,IAAI,CA6FhF"}
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { READ_ONLY_TOOL_ANNOTATIONS } from './annotations.js';
3
3
  import { ErrorCode, throwMcpError } from './errors.js';
4
+ import { resolveNodeWorldPoint } from './node-world-point.js';
4
5
  import { NodeIdSchema } from './schemas.js';
5
6
  export const measureInput = {
6
7
  fromId: NodeIdSchema,
@@ -8,66 +9,13 @@ export const measureInput = {
8
9
  };
9
10
  export const measureOutput = {
10
11
  distanceMeters: z.number(),
12
+ fromPoint: z.array(z.number()).optional(),
13
+ toPoint: z.array(z.number()).optional(),
14
+ approximate: z.array(z.object({ id: z.string(), reason: z.string() })).optional(),
11
15
  areaSqMeters: z.number().optional(),
12
16
  units: z.literal('meters'),
13
17
  areaUnits: z.literal('square_meters').optional(),
14
18
  };
15
- /**
16
- * Compute a 3D centre point in level-coordinate space for distance measurement.
17
- *
18
- * For walls / fences: midpoint of the start/end segment at Y=0.
19
- * For positioned nodes (item, door, window, building, stair, roof): `position`.
20
- * For polygon nodes (slab, ceiling, zone): 2D centroid lifted to Y=0.
21
- * For site: centroid of property line at Y=0 if available.
22
- *
23
- * Returns null if no representative centre can be derived (e.g. level node
24
- * has no position of its own).
25
- */
26
- function getCentre(node) {
27
- switch (node.type) {
28
- case 'wall':
29
- case 'fence': {
30
- const [x1, z1] = node.start;
31
- const [x2, z2] = node.end;
32
- return [(x1 + x2) / 2, 0, (z1 + z2) / 2];
33
- }
34
- case 'item':
35
- case 'door':
36
- case 'window':
37
- case 'building':
38
- case 'stair':
39
- case 'roof':
40
- return node.position;
41
- case 'slab':
42
- case 'ceiling':
43
- case 'zone': {
44
- const poly = node.polygon;
45
- if (!poly || poly.length === 0)
46
- return null;
47
- let cx = 0;
48
- let cz = 0;
49
- for (const [x, z] of poly) {
50
- cx += x;
51
- cz += z;
52
- }
53
- return [cx / poly.length, 0, cz / poly.length];
54
- }
55
- case 'site': {
56
- const pts = node.polygon?.points ?? [];
57
- if (pts.length === 0)
58
- return [0, 0, 0];
59
- let cx = 0;
60
- let cz = 0;
61
- for (const [x, z] of pts) {
62
- cx += x;
63
- cz += z;
64
- }
65
- return [cx / pts.length, 0, cz / pts.length];
66
- }
67
- default:
68
- return null;
69
- }
70
- }
71
19
  /** Compute polygon area via the shoelace formula. */
72
20
  function shoelaceArea(polygon) {
73
21
  if (polygon.length < 3)
@@ -84,7 +32,7 @@ function shoelaceArea(polygon) {
84
32
  export function registerMeasure(server, bridge) {
85
33
  server.registerTool('measure', {
86
34
  title: 'Measure',
87
- description: 'Measure distance (in meters) between two nodes, or the net area of a polygon node when fromId === toId.',
35
+ description: "Measure the distance (in meters) between two nodes, or the net area of a polygon node when fromId === toId. Distance is between world-space reference points, returned as fromPoint/toPoint: a positioned node's origin in its host's frame (an item's base, a door's or window's centre in its wall), a wall's or fence's midpoint at its base, a slab's, ceiling's or zone's polygon centroid on its plane, a block's or imported mesh's vertex-bounds centre, a level's plan centre on its base plane, and another container's descendants' centre. `approximate` lists nodes whose renderer derives the pose from data not modelled here (a downspout at its gutter outlet, a gutter at the eave), with the reason.",
88
36
  inputSchema: measureInput,
89
37
  outputSchema: measureOutput,
90
38
  annotations: READ_ONLY_TOOL_ANNOTATIONS,
@@ -124,17 +72,37 @@ export function registerMeasure(server, bridge) {
124
72
  structuredContent: payload,
125
73
  };
126
74
  }
127
- const fromCentre = getCentre(from);
128
- const toCentre = getCentre(to);
129
- if (!(fromCentre && toCentre)) {
130
- throwMcpError(ErrorCode.InvalidRequest, `Cannot derive centre for measurement between ${from.type} and ${to.type}`);
131
- }
132
- const dx = fromCentre[0] - toCentre[0];
133
- const dy = fromCentre[1] - toCentre[1];
134
- const dz = fromCentre[2] - toCentre[2];
75
+ const nodes = bridge.getNodes();
76
+ const pointOf = (node) => {
77
+ try {
78
+ const point = resolveNodeWorldPoint(node.id, nodes);
79
+ if (point)
80
+ return point;
81
+ }
82
+ catch (err) {
83
+ const reason = err instanceof Error ? err.message : String(err);
84
+ throwMcpError(ErrorCode.InvalidRequest, `Cannot derive a world point for ${node.type} ${node.id}: ${reason}`);
85
+ }
86
+ throwMcpError(ErrorCode.InvalidRequest, `Cannot derive a world point for ${node.type} ${node.id}`);
87
+ };
88
+ const fromResolved = pointOf(from);
89
+ const toResolved = pointOf(to);
90
+ const fromPoint = fromResolved.point;
91
+ const toPoint = toResolved.point;
92
+ const approximate = [];
93
+ if (fromResolved.approximate)
94
+ approximate.push({ id: from.id, reason: fromResolved.approximate });
95
+ if (toResolved.approximate)
96
+ approximate.push({ id: to.id, reason: toResolved.approximate });
97
+ const dx = fromPoint[0] - toPoint[0];
98
+ const dy = fromPoint[1] - toPoint[1];
99
+ const dz = fromPoint[2] - toPoint[2];
135
100
  const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
136
101
  const payload = {
137
102
  distanceMeters: distance,
103
+ fromPoint,
104
+ toPoint,
105
+ ...(approximate.length > 0 ? { approximate } : {}),
138
106
  units: 'meters',
139
107
  };
140
108
  return {
@@ -1,37 +1,2 @@
1
- import { type Kind } from '@pascal-app/lingo';
2
- import { z } from 'zod';
3
- /**
4
- * A zod field for a measurement tool argument that a model may emit as a bare
5
- * number OR as natural language. `measurement('length', 'm')` accepts `0.15`,
6
- * `"6 in"`, `"180cm"`, `"2 ft 3 in"`; `measurement('angle', 'deg')` accepts
7
- * `45`, `"45°"`, `"1.57rad"`, `"0.25 turn"`. The value is canonicalized (via
8
- * `@pascal-app/lingo`) to a number in `unit` — the exact unit the tool handler
9
- * already expects — so no handler change is needed. `min`/`max` (in `unit`)
10
- * reject out-of-range values with a model-readable message.
11
- *
12
- * The emitted JSON Schema is `number | string`, so the model is free to answer
13
- * in whatever unit it is thinking in; AI SDK v6 applies the transform and
14
- * forwards the canonical number to the tool executor.
15
- *
16
- * Tool-boundary safety: genuinely ambiguous separators (`"1,234"`, which could
17
- * mean 1234 or 1.234) are rejected rather than silently absorbed, so a European
18
- * decimal never becomes a 1000× value.
19
- *
20
- * NOTE: intentionally duplicated as `editor/packages/mcp/src/tools/measurement.ts`
21
- * (MCP tools) and `packages/ai/src/tools/scene/measurement.ts` (AI-chat tools).
22
- * The two tool stacks live in different packages — one inside the `editor`
23
- * submodule — so they can't share a module. Keep the two copies identical; the
24
- * clean long-term dedup is a zod-field adapter exported from `@pascal-app/lingo`.
25
- */
26
- export interface MeasurementOptions {
27
- /** Semantic description (e.g. "Wall thickness"). The natural-language note is appended. */
28
- description?: string;
29
- /** Upper bound, in `unit`. */
30
- max?: number;
31
- /** Inclusive lower bound, in `unit`. */
32
- min?: number;
33
- /** Require the value to be strictly greater than 0 (for non-zero dimensions). */
34
- positive?: boolean;
35
- }
36
- export declare function measurement(kind: Kind, unit: string, opts?: MeasurementOptions): z.ZodPipe<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>, z.ZodTransform<number, string | number>>;
1
+ export { measurement } from '@pascal-app/core/agent-tools';
37
2
  //# sourceMappingURL=measurement.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"measurement.d.ts","sourceRoot":"","sources":["../../src/tools/measurement.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAiB,MAAM,mBAAmB,CAAA;AAC5D,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,MAAM,WAAW,kBAAkB;IACjC,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,8BAA8B;IAC9B,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,wCAAwC;IACxC,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,iFAAiF;IACjF,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AA6CD,wBAAgB,WAAW,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,uGAoDlF"}
1
+ {"version":3,"file":"measurement.d.ts","sourceRoot":"","sources":["../../src/tools/measurement.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA"}
@@ -1,94 +1,2 @@
1
- import { parseQuantity } from '@pascal-app/lingo';
2
- import { z } from 'zod';
3
- function unitNoun(unit) {
4
- switch (unit) {
5
- case 'm':
6
- return 'meters';
7
- case 'deg':
8
- return 'degrees';
9
- case 'rad':
10
- return 'radians';
11
- default:
12
- return unit;
13
- }
14
- }
15
- function naturalLanguageNote(kind, unit) {
16
- let examples;
17
- if (kind === 'angle') {
18
- // Lead with an example in the field's own unit so a bare number is read
19
- // the way the model intends (a bare "45" in a radians field is 45 rad).
20
- examples =
21
- unit === 'rad' ? '1.5708, "90°", "1.57rad", "0.25 turn"' : '45, "45°", "1.57rad", "0.25 turn"';
22
- }
23
- else {
24
- examples = '0.9, "6 ft", "180cm", "2 ft 3 in"';
25
- }
26
- return `Accepts a number (${unitNoun(unit)}) or a natural-language string; other units are converted. e.g. ${examples}.`;
27
- }
28
- function boundsNote(unit, min, max, positive) {
29
- if (positive) {
30
- return max === undefined
31
- ? ` Must be greater than 0 ${unit}.`
32
- : ` Greater than 0, up to ${max} ${unit}.`;
33
- }
34
- if (min !== undefined && max !== undefined)
35
- return ` Range ${min}–${max} ${unit}.`;
36
- if (min !== undefined)
37
- return ` Minimum ${min} ${unit}.`;
38
- if (max !== undefined)
39
- return ` Maximum ${max} ${unit}.`;
40
- return '';
41
- }
42
- export function measurement(kind, unit, opts = {}) {
43
- const { min, max, positive = false } = opts;
44
- const description = [
45
- opts.description,
46
- naturalLanguageNote(kind, unit) + boundsNote(unit, min, max, positive),
47
- ]
48
- .filter(Boolean)
49
- .join(' ');
50
- return z
51
- .union([z.number(), z.string()])
52
- .transform((val, ctx) => {
53
- let value;
54
- if (typeof val === 'number') {
55
- value = val;
56
- }
57
- else {
58
- const result = parseQuantity(val, {
59
- kind,
60
- unit,
61
- strictness: 'forgiving',
62
- // A genuinely ambiguous separator ("1,234") must not silently become
63
- // a 1000× value at a tool boundary — fail so the model self-corrects.
64
- escalate: { AMBIGUOUS_NUMBER: 'error' },
65
- });
66
- if (!result.ok) {
67
- ctx.addIssue({
68
- code: 'custom',
69
- message: result.issues[0]?.message ?? `Could not read "${val}" as a ${kind}.`,
70
- });
71
- return z.NEVER;
72
- }
73
- value = result.quantity.to(unit).value;
74
- }
75
- if (!Number.isFinite(value)) {
76
- ctx.addIssue({ code: 'custom', message: `Value must be a finite ${kind} in ${unit}.` });
77
- return z.NEVER;
78
- }
79
- if (positive && value <= 0) {
80
- ctx.addIssue({ code: 'custom', message: `Must be greater than 0 ${unit} (got ${value}).` });
81
- return z.NEVER;
82
- }
83
- if (min !== undefined && value < min) {
84
- ctx.addIssue({ code: 'custom', message: `Must be at least ${min} ${unit} (got ${value}).` });
85
- return z.NEVER;
86
- }
87
- if (max !== undefined && value > max) {
88
- ctx.addIssue({ code: 'custom', message: `Must be at most ${max} ${unit} (got ${value}).` });
89
- return z.NEVER;
90
- }
91
- return value;
92
- })
93
- .describe(description);
94
- }
1
+ // The one measurement() helper, shared with the hosted chat through the agent tool contracts.
2
+ export { measurement } from '@pascal-app/core/agent-tools';
@@ -0,0 +1,37 @@
1
+ import type { Vec3 } from '@pascal-app/core/agent-operations';
2
+ import { AnyNode } from '@pascal-app/core/schema';
3
+ type Nodes = Readonly<Record<string, AnyNode>>;
4
+ /**
5
+ * Built-in kinds the viewer lifts onto the slab under them through the
6
+ * `floorPlaced` capability of their registered definition, and whose footprint
7
+ * core cannot derive without it (core derives item, shelf and procedural-item
8
+ * footprints from the node itself). Headless MCP does not load
9
+ * `@pascal-app/nodes`, so their lift is unresolved and flagged, not guessed. A
10
+ * test keeps this list in step with the definitions.
11
+ */
12
+ export declare const HEADLESS_UNRESOLVED_FLOOR_LIFT_KINDS: readonly ["block", "cabinet", "cabinet-module", "column", "duct-terminal", "hvac-equipment", "spawn", "stair"];
13
+ /**
14
+ * World-space reference point of any node, following the frames the viewer
15
+ * renders it in (core's `nodeLevelFrame`, the level's stacked base Y and its
16
+ * building transform):
17
+ * - a positioned node: its origin (an item's or column's base, a door's or
18
+ * window's centre), inside its host's frame;
19
+ * - a wall or fence: the midpoint of its centreline (arc or spline) at its base;
20
+ * - a grid line: its midpoint;
21
+ * - a slab, ceiling, zone or site: its polygon centroid on its own plane;
22
+ * - a block or imported mesh: the centre of its vertex bounds;
23
+ * - a path or measurement: the centre of its points;
24
+ * - a level: the plan centre of its content on its base plane;
25
+ * - another container (a unit, …): the centre of its descendants' points,
26
+ * or its origin when it has none.
27
+ * Kinds whose renderer derives the pose elsewhere (a downspout at its gutter
28
+ * outlet, a gutter at the eave, …) are returned with an `approximate` reason.
29
+ */
30
+ export type NodeWorldPoint = {
31
+ point: Vec3;
32
+ /** Set when the renderer derives this kind's pose from data not modelled here. */
33
+ approximate?: string;
34
+ };
35
+ export declare function resolveNodeWorldPoint(id: string, nodes: Nodes): NodeWorldPoint | null;
36
+ export {};
37
+ //# sourceMappingURL=node-world-point.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-world-point.d.ts","sourceRoot":"","sources":["../../src/tools/node-world-point.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mCAAmC,CAAA;AAS7D,OAAO,EACL,OAAO,EAIR,MAAM,yBAAyB,CAAA;AAEhC,KAAK,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAgN9C;;;;;;;GAOG;AACH,eAAO,MAAM,oCAAoC,gHASvC,CAAA;AA6KV;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,IAAI,CAAA;IACX,kFAAkF;IAClF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,GAAG,cAAc,GAAG,IAAI,CAMrF"}