fluidcad 0.0.32 → 0.0.34

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 (279) hide show
  1. package/README.md +3 -2
  2. package/bin/commands/init.js +55 -0
  3. package/bin/commands/mcp.js +33 -0
  4. package/bin/commands/serve.js +77 -0
  5. package/bin/fluidcad.js +15 -107
  6. package/lib/dist/common/scene-object.d.ts +4 -1
  7. package/lib/dist/common/scene-object.js +9 -2
  8. package/lib/dist/common/solid.d.ts +4 -1
  9. package/lib/dist/common/solid.js +13 -0
  10. package/lib/dist/core/2d/tarc.d.ts +20 -2
  11. package/lib/dist/core/2d/tarc.js +24 -0
  12. package/lib/dist/core/index.d.ts +2 -1
  13. package/lib/dist/core/index.js +1 -0
  14. package/lib/dist/core/interfaces.d.ts +107 -2
  15. package/lib/dist/core/load.d.ts +2 -2
  16. package/lib/dist/core/repeat.js +62 -46
  17. package/lib/dist/core/rib.d.ts +18 -0
  18. package/lib/dist/core/rib.js +37 -0
  19. package/lib/dist/features/2d/arc.d.ts +8 -2
  20. package/lib/dist/features/2d/arc.js +94 -17
  21. package/lib/dist/features/2d/back.js +3 -2
  22. package/lib/dist/features/2d/sketch.d.ts +4 -0
  23. package/lib/dist/features/2d/sketch.js +21 -0
  24. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  25. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  26. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  27. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  28. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  29. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  30. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  31. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  32. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  33. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  34. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  35. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  36. package/lib/dist/features/2d/tarc.d.ts +2 -0
  37. package/lib/dist/features/2d/tarc.js +3 -0
  38. package/lib/dist/features/extrude-base.d.ts +9 -0
  39. package/lib/dist/features/extrude-base.js +22 -0
  40. package/lib/dist/features/extrude-to-face.js +1 -5
  41. package/lib/dist/features/extrude-two-distances.js +1 -2
  42. package/lib/dist/features/extrude.js +1 -2
  43. package/lib/dist/features/load.d.ts +6 -0
  44. package/lib/dist/features/load.js +53 -1
  45. package/lib/dist/features/mirror-feature.d.ts +3 -2
  46. package/lib/dist/features/mirror-feature.js +1 -1
  47. package/lib/dist/features/repeat-circular.d.ts +3 -3
  48. package/lib/dist/features/repeat-circular.js +8 -1
  49. package/lib/dist/features/repeat-linear.d.ts +4 -2
  50. package/lib/dist/features/repeat-linear.js +10 -1
  51. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  52. package/lib/dist/features/repeat-matrix.js +7 -2
  53. package/lib/dist/features/rib.d.ts +31 -0
  54. package/lib/dist/features/rib.js +321 -0
  55. package/lib/dist/features/select.d.ts +1 -0
  56. package/lib/dist/features/select.js +81 -10
  57. package/lib/dist/features/shell.d.ts +4 -1
  58. package/lib/dist/features/shell.js +14 -3
  59. package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
  60. package/lib/dist/filters/edge/belongs-to-face.js +64 -15
  61. package/lib/dist/filters/filter-builder-base.d.ts +25 -0
  62. package/lib/dist/filters/filter-builder-base.js +47 -0
  63. package/lib/dist/filters/filter.js +39 -14
  64. package/lib/dist/filters/from-object.d.ts +4 -0
  65. package/lib/dist/filters/from-object.js +10 -0
  66. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  67. package/lib/dist/helpers/scene-helpers.d.ts +1 -1
  68. package/lib/dist/helpers/scene-helpers.js +146 -12
  69. package/lib/dist/index.d.ts +7 -1
  70. package/lib/dist/index.js +3 -3
  71. package/lib/dist/io/file-import.d.ts +5 -1
  72. package/lib/dist/io/file-import.js +29 -18
  73. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  74. package/lib/dist/math/lazy-matrix.js +66 -0
  75. package/lib/dist/oc/color-transfer.d.ts +19 -8
  76. package/lib/dist/oc/color-transfer.js +70 -12
  77. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  78. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  79. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  80. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  81. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  82. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  83. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  84. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  85. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  86. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  87. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  88. package/lib/dist/oc/extrude-ops.d.ts +2 -1
  89. package/lib/dist/oc/extrude-ops.js +51 -2
  90. package/lib/dist/oc/mesh.d.ts +9 -4
  91. package/lib/dist/oc/mesh.js +14 -13
  92. package/lib/dist/oc/rib-ops.d.ts +35 -0
  93. package/lib/dist/oc/rib-ops.js +619 -0
  94. package/lib/dist/oc/shell-ops.d.ts +2 -1
  95. package/lib/dist/oc/shell-ops.js +5 -2
  96. package/lib/dist/oc/topology-index.d.ts +6 -0
  97. package/lib/dist/oc/topology-index.js +36 -0
  98. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  99. package/lib/dist/rendering/mesh-builder.js +8 -4
  100. package/lib/dist/rendering/render-edge.d.ts +2 -1
  101. package/lib/dist/rendering/render-edge.js +2 -2
  102. package/lib/dist/rendering/render-face.d.ts +2 -1
  103. package/lib/dist/rendering/render-face.js +2 -2
  104. package/lib/dist/rendering/render-solid.d.ts +2 -1
  105. package/lib/dist/rendering/render-solid.js +3 -5
  106. package/lib/dist/rendering/render-wire.d.ts +2 -1
  107. package/lib/dist/rendering/render-wire.js +2 -2
  108. package/lib/dist/rendering/render.d.ts +4 -0
  109. package/lib/dist/rendering/render.js +50 -2
  110. package/lib/dist/rendering/scene-compare.js +3 -0
  111. package/lib/dist/rendering/scene.d.ts +1 -0
  112. package/lib/dist/rendering/scene.js +4 -0
  113. package/lib/dist/scene-manager.d.ts +4 -2
  114. package/lib/dist/scene-manager.js +12 -4
  115. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  116. package/lib/dist/tests/features/2d/back.test.js +17 -1
  117. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  118. package/lib/dist/tests/features/color-lineage.test.js +18 -0
  119. package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
  120. package/lib/dist/tests/features/filter-positional.test.js +129 -0
  121. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  122. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  123. package/lib/dist/tests/features/rib.test.d.ts +1 -0
  124. package/lib/dist/tests/features/rib.test.js +598 -0
  125. package/lib/dist/tests/features/shell.test.js +36 -0
  126. package/lib/dist/tests/global-setup.js +2 -1
  127. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  128. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  129. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  130. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  131. package/lib/dist/tests/scene-compare.test.d.ts +1 -0
  132. package/lib/dist/tests/scene-compare.test.js +77 -0
  133. package/lib/dist/tests/setup.js +2 -1
  134. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  135. package/llm-docs/.coverage-allowlist.txt +9 -0
  136. package/llm-docs/api/arc.md +48 -0
  137. package/llm-docs/api/axis.md +42 -0
  138. package/llm-docs/api/bezier.md +42 -0
  139. package/llm-docs/api/booleans.md +44 -0
  140. package/llm-docs/api/chamfer.md +40 -0
  141. package/llm-docs/api/circle.md +36 -0
  142. package/llm-docs/api/color.md +34 -0
  143. package/llm-docs/api/connect.md +41 -0
  144. package/llm-docs/api/constraint-qualifiers.md +48 -0
  145. package/llm-docs/api/copy.md +63 -0
  146. package/llm-docs/api/cursor-lines.md +50 -0
  147. package/llm-docs/api/cursor-move.md +61 -0
  148. package/llm-docs/api/cut.md +55 -0
  149. package/llm-docs/api/draft.md +36 -0
  150. package/llm-docs/api/edge-filter.md +57 -0
  151. package/llm-docs/api/ellipse.md +34 -0
  152. package/llm-docs/api/extrude.md +74 -0
  153. package/llm-docs/api/face-filter.md +61 -0
  154. package/llm-docs/api/fillet.md +51 -0
  155. package/llm-docs/api/index.json +139 -0
  156. package/llm-docs/api/line.md +42 -0
  157. package/llm-docs/api/load.md +37 -0
  158. package/llm-docs/api/local.md +38 -0
  159. package/llm-docs/api/loft.md +37 -0
  160. package/llm-docs/api/mirror.md +44 -0
  161. package/llm-docs/api/offset.md +36 -0
  162. package/llm-docs/api/part.md +40 -0
  163. package/llm-docs/api/plane.md +44 -0
  164. package/llm-docs/api/polygon.md +37 -0
  165. package/llm-docs/api/primitive-solids.md +39 -0
  166. package/llm-docs/api/project-intersect.md +48 -0
  167. package/llm-docs/api/rect.md +48 -0
  168. package/llm-docs/api/remove.md +32 -0
  169. package/llm-docs/api/repeat.md +79 -0
  170. package/llm-docs/api/revolve.md +38 -0
  171. package/llm-docs/api/rib.md +40 -0
  172. package/llm-docs/api/rotate.md +37 -0
  173. package/llm-docs/api/select.md +41 -0
  174. package/llm-docs/api/shell.md +41 -0
  175. package/llm-docs/api/sketch.md +76 -0
  176. package/llm-docs/api/slot.md +36 -0
  177. package/llm-docs/api/split-trim.md +42 -0
  178. package/llm-docs/api/sweep.md +43 -0
  179. package/llm-docs/api/tarc.md +45 -0
  180. package/llm-docs/api/tcircle.md +38 -0
  181. package/llm-docs/api/tline.md +42 -0
  182. package/llm-docs/api/translate.md +40 -0
  183. package/llm-docs/api/types/aline.md +35 -0
  184. package/llm-docs/api/types/arc-angles.md +29 -0
  185. package/llm-docs/api/types/arc-points.md +48 -0
  186. package/llm-docs/api/types/axis-like.md +38 -0
  187. package/llm-docs/api/types/axis.md +21 -0
  188. package/llm-docs/api/types/boolean-operation.md +50 -0
  189. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  190. package/llm-docs/api/types/common.md +32 -0
  191. package/llm-docs/api/types/cut.md +125 -0
  192. package/llm-docs/api/types/draft.md +21 -0
  193. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  194. package/llm-docs/api/types/extrude.md +194 -0
  195. package/llm-docs/api/types/geometry.md +51 -0
  196. package/llm-docs/api/types/hline.md +35 -0
  197. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  198. package/llm-docs/api/types/loft.md +154 -0
  199. package/llm-docs/api/types/mirror.md +35 -0
  200. package/llm-docs/api/types/offset.md +31 -0
  201. package/llm-docs/api/types/plane-like.md +35 -0
  202. package/llm-docs/api/types/plane-transform-options.md +29 -0
  203. package/llm-docs/api/types/plane.md +21 -0
  204. package/llm-docs/api/types/point-like.md +22 -0
  205. package/llm-docs/api/types/point2dlike.md +26 -0
  206. package/llm-docs/api/types/polygon.md +46 -0
  207. package/llm-docs/api/types/rect.md +128 -0
  208. package/llm-docs/api/types/revolve.md +102 -0
  209. package/llm-docs/api/types/rib.md +133 -0
  210. package/llm-docs/api/types/scene-object.md +33 -0
  211. package/llm-docs/api/types/select.md +21 -0
  212. package/llm-docs/api/types/shell.md +54 -0
  213. package/llm-docs/api/types/slot.md +43 -0
  214. package/llm-docs/api/types/sweep.md +189 -0
  215. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  216. package/llm-docs/api/types/transformable.md +93 -0
  217. package/llm-docs/api/types/trim.md +27 -0
  218. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  219. package/llm-docs/api/types/vertex.md +17 -0
  220. package/llm-docs/api/types/vline.md +35 -0
  221. package/llm-docs/concepts/coordinate-system.md +45 -0
  222. package/llm-docs/concepts/history-and-rollback.md +40 -0
  223. package/llm-docs/concepts/last-selection.md +49 -0
  224. package/llm-docs/concepts/scene-graph.md +37 -0
  225. package/llm-docs/index.json +1750 -0
  226. package/mcp/dist/client.d.ts +64 -0
  227. package/mcp/dist/client.js +248 -0
  228. package/mcp/dist/discovery.d.ts +11 -0
  229. package/mcp/dist/discovery.js +78 -0
  230. package/mcp/dist/docs-index.d.ts +81 -0
  231. package/mcp/dist/docs-index.js +261 -0
  232. package/mcp/dist/resources.d.ts +4 -0
  233. package/mcp/dist/resources.js +115 -0
  234. package/mcp/dist/server.d.ts +12 -0
  235. package/mcp/dist/server.js +489 -0
  236. package/mcp/dist/tools/coordination.d.ts +9 -0
  237. package/mcp/dist/tools/coordination.js +46 -0
  238. package/mcp/dist/tools/docs.d.ts +66 -0
  239. package/mcp/dist/tools/docs.js +122 -0
  240. package/mcp/dist/tools/engine.d.ts +56 -0
  241. package/mcp/dist/tools/engine.js +145 -0
  242. package/mcp/dist/tools/inspection.d.ts +75 -0
  243. package/mcp/dist/tools/inspection.js +121 -0
  244. package/mcp/dist/tools/screenshot.d.ts +63 -0
  245. package/mcp/dist/tools/screenshot.js +263 -0
  246. package/mcp/dist/tools/source.d.ts +84 -0
  247. package/mcp/dist/tools/source.js +434 -0
  248. package/mcp/dist/tools/workspaces.d.ts +13 -0
  249. package/mcp/dist/tools/workspaces.js +33 -0
  250. package/mcp/dist/types.d.ts +18 -0
  251. package/mcp/dist/types.js +11 -0
  252. package/package.json +19 -5
  253. package/server/dist/code-editor.d.ts +36 -0
  254. package/server/dist/code-editor.js +8 -0
  255. package/server/dist/fluidcad-server.d.ts +50 -0
  256. package/server/dist/fluidcad-server.js +153 -1
  257. package/server/dist/global-registry.d.ts +30 -0
  258. package/server/dist/global-registry.js +126 -0
  259. package/server/dist/index.js +171 -26
  260. package/server/dist/instance-file.d.ts +31 -0
  261. package/server/dist/instance-file.js +73 -0
  262. package/server/dist/lint-fluid-js.d.ts +15 -0
  263. package/server/dist/lint-fluid-js.js +271 -0
  264. package/server/dist/routes/editor.d.ts +24 -0
  265. package/server/dist/routes/editor.js +44 -0
  266. package/server/dist/routes/export.d.ts +1 -1
  267. package/server/dist/routes/export.js +45 -8
  268. package/server/dist/routes/health.d.ts +7 -0
  269. package/server/dist/routes/health.js +14 -0
  270. package/server/dist/routes/lint.d.ts +10 -0
  271. package/server/dist/routes/lint.js +28 -0
  272. package/server/dist/routes/render.d.ts +33 -0
  273. package/server/dist/routes/render.js +34 -0
  274. package/server/dist/routes/scene.d.ts +5 -0
  275. package/server/dist/routes/scene.js +48 -0
  276. package/server/dist/routes/screenshot.js +68 -1
  277. package/server/dist/ws-protocol.d.ts +56 -2
  278. package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
  279. package/ui/dist/index.html +1 -1
@@ -0,0 +1,271 @@
1
+ // Static import linter for `.fluid.js` sources.
2
+ //
3
+ // LLMs frequently emit a first draft that uses FluidCAD APIs (`sketch`,
4
+ // `extrude`, `face`, …) without an `import { … } from "fluidcad/core"` line —
5
+ // the script then explodes at runtime with `ReferenceError`. The MCP server
6
+ // calls this linter before writing a `.fluid.js` file so the agent gets a
7
+ // precise error pointing at the missing symbols instead of a confusing
8
+ // render failure.
9
+ //
10
+ // Tree-sitter (web-tree-sitter + tree-sitter-wasms) is reused — the parser
11
+ // instance is the same singleton the param editor in `code-editor.ts` uses.
12
+ // Doing it that way means we don't double-load the JavaScript wasm grammar.
13
+ import { getJavaScriptParser } from "./code-editor.js";
14
+ // Authoritative FluidCAD symbol→module map. Mirrors the exports in
15
+ // `lib/core/index.ts`, `lib/core/2d/index.ts`, `lib/filters/index.ts`, and
16
+ // `lib/features/2d/constraints/geometry-qualifier.ts`. If a new public symbol
17
+ // is added there, add it here too — the import lint is the wall the LLM
18
+ // hits, so keep it accurate.
19
+ const CORE_SYMBOLS = new Set([
20
+ 'axis', 'local', 'plane', 'sketch', 'fuse', 'subtract', 'common',
21
+ 'cut', 'revolve', 'extrude', 'sphere', 'cylinder', 'select', 'shell',
22
+ 'chamfer', 'fillet', 'translate', 'rotate', 'mirror', 'copy', 'repeat',
23
+ 'load', 'loft', 'sweep', 'rib', 'color', 'draft', 'remove', 'split',
24
+ 'trim', 'part', 'breakpoint',
25
+ 'line', 'circle', 'ellipse', 'rect', 'hMove', 'vMove', 'rMove',
26
+ 'hLine', 'vLine', 'tLine', 'tCircle', 'tArc', 'arc', 'move', 'pMove',
27
+ 'aLine', 'slot', 'connect', 'polygon', 'offset', 'project', 'intersect',
28
+ 'bezier', 'center', 'back',
29
+ ]);
30
+ const FILTER_SYMBOLS = new Set(['face', 'edge']);
31
+ const CONSTRAINT_SYMBOLS = new Set([
32
+ 'outside', 'enclosed', 'enclosing', 'unqualified',
33
+ ]);
34
+ const MODULE_FOR_SYMBOL = new Map();
35
+ for (const s of CORE_SYMBOLS) {
36
+ MODULE_FOR_SYMBOL.set(s, 'fluidcad/core');
37
+ }
38
+ for (const s of FILTER_SYMBOLS) {
39
+ MODULE_FOR_SYMBOL.set(s, 'fluidcad/filters');
40
+ }
41
+ for (const s of CONSTRAINT_SYMBOLS) {
42
+ MODULE_FOR_SYMBOL.set(s, 'fluidcad/constraints');
43
+ }
44
+ /**
45
+ * Walk every named child recursively, invoking `visit` once per node. The
46
+ * walker stops descending into a subtree when `visit` returns `false`.
47
+ */
48
+ function walk(node, visit) {
49
+ const cont = visit(node);
50
+ if (cont === false) {
51
+ return;
52
+ }
53
+ for (const child of node.namedChildren) {
54
+ walk(child, visit);
55
+ }
56
+ }
57
+ /**
58
+ * Determine whether `node` (an `identifier`) is at a position the JS spec
59
+ * treats as a *reference* (would throw `ReferenceError` if unbound) vs a
60
+ * *binding* (a declared name) vs a *property name* (never a reference at
61
+ * all).
62
+ *
63
+ * Tree-sitter's JavaScript grammar has distinct node types for some of
64
+ * these (`property_identifier`, `shorthand_property_identifier_pattern`,
65
+ * etc.), but plain `identifier` nodes still cover a lot of ground and we
66
+ * have to look at parent context to classify them.
67
+ */
68
+ function isReferenceUse(node) {
69
+ if (node.type !== 'identifier') {
70
+ return false;
71
+ }
72
+ const parent = node.parent;
73
+ if (!parent) {
74
+ return true;
75
+ }
76
+ switch (parent.type) {
77
+ case 'import_specifier':
78
+ case 'namespace_import':
79
+ case 'import_clause':
80
+ case 'import_statement':
81
+ // Anything inside an `import` statement is bookkeeping, not a use.
82
+ return false;
83
+ case 'variable_declarator': {
84
+ // `const X = ...` → the `name` field is a binding.
85
+ const nameField = parent.childForFieldName('name');
86
+ if (nameField && nameField.startIndex === node.startIndex) {
87
+ return false;
88
+ }
89
+ return true;
90
+ }
91
+ case 'function_declaration':
92
+ case 'function_expression':
93
+ case 'generator_function_declaration':
94
+ case 'class_declaration':
95
+ case 'class_expression':
96
+ case 'method_definition': {
97
+ const nameField = parent.childForFieldName('name');
98
+ if (nameField && nameField.startIndex === node.startIndex) {
99
+ return false;
100
+ }
101
+ return true;
102
+ }
103
+ case 'formal_parameters':
104
+ case 'required_parameter':
105
+ case 'optional_parameter':
106
+ case 'rest_pattern':
107
+ // Parameter names are bindings.
108
+ return false;
109
+ case 'arrow_function': {
110
+ // `(x) => …` or `x => …` — parameter is a binding when it's the
111
+ // function's `parameter` field.
112
+ const param = parent.childForFieldName('parameter');
113
+ if (param && param.startIndex === node.startIndex) {
114
+ return false;
115
+ }
116
+ return true;
117
+ }
118
+ case 'member_expression': {
119
+ // `obj.foo` — the `property` field is a name lookup, not a reference.
120
+ const prop = parent.childForFieldName('property');
121
+ if (prop && prop.startIndex === node.startIndex) {
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ case 'pair': {
127
+ // `{ key: value }` — `key` (if an identifier) is a property name.
128
+ const key = parent.childForFieldName('key');
129
+ if (key && key.startIndex === node.startIndex) {
130
+ return false;
131
+ }
132
+ return true;
133
+ }
134
+ case 'property_signature':
135
+ case 'public_field_definition':
136
+ // Class/object field declarations — the name slot is a binding.
137
+ return false;
138
+ case 'labeled_statement':
139
+ // `label: stmt` — labels are not references.
140
+ return false;
141
+ }
142
+ return true;
143
+ }
144
+ /**
145
+ * Walk an `import_statement` and collect every name it locally binds.
146
+ * Handles:
147
+ * - `import x from "…"` (default)
148
+ * - `import { a, b as c } from "…"` (named, with renaming)
149
+ * - `import x, { a } from "…"` (default + named)
150
+ * - `import * as ns from "…"` (namespace)
151
+ * - `import "…"` (side-effect, binds nothing)
152
+ */
153
+ function collectImportedNames(importNode, into) {
154
+ walk(importNode, (n) => {
155
+ if (n.type === 'import_specifier') {
156
+ // `{ a as b }` → `alias` field (b) is what binds; otherwise `name` (a).
157
+ const alias = n.childForFieldName('alias');
158
+ const name = n.childForFieldName('name');
159
+ const local = alias ?? name;
160
+ if (local && local.type === 'identifier') {
161
+ into.add(local.text);
162
+ }
163
+ return false;
164
+ }
165
+ if (n.type === 'namespace_import') {
166
+ const id = n.namedChildren.find((c) => c.type === 'identifier');
167
+ if (id) {
168
+ into.add(id.text);
169
+ }
170
+ return false;
171
+ }
172
+ if (n.type === 'import_clause') {
173
+ // The default import (if any) is a direct `identifier` child.
174
+ for (const child of n.namedChildren) {
175
+ if (child.type === 'identifier') {
176
+ into.add(child.text);
177
+ }
178
+ }
179
+ }
180
+ });
181
+ }
182
+ /**
183
+ * Collect names that are bound at the top level of the program: `const`,
184
+ * `let`, `var`, `function`, `class`. Used so user code like
185
+ * `const sketch = …` shadows the FluidCAD `sketch` without tripping the
186
+ * lint. We deliberately keep this top-level-only to keep the implementation
187
+ * simple — local shadowing inside a function is rare in `.fluid.js` files.
188
+ */
189
+ function collectTopLevelDeclaredNames(root, into) {
190
+ for (const stmt of root.namedChildren) {
191
+ if (stmt.type === 'variable_declaration' || stmt.type === 'lexical_declaration') {
192
+ for (const decl of stmt.namedChildren) {
193
+ if (decl.type !== 'variable_declarator') {
194
+ continue;
195
+ }
196
+ const name = decl.childForFieldName('name');
197
+ if (name && name.type === 'identifier') {
198
+ into.add(name.text);
199
+ }
200
+ // Destructuring (`const { a, b } = …`) — pick up shorthand pattern names.
201
+ if (name) {
202
+ walk(name, (n) => {
203
+ if (n.type === 'shorthand_property_identifier_pattern' ||
204
+ n.type === 'identifier') {
205
+ if (n.parent && n.parent.type !== 'pair_pattern') {
206
+ into.add(n.text);
207
+ }
208
+ }
209
+ });
210
+ }
211
+ }
212
+ }
213
+ if (stmt.type === 'function_declaration' || stmt.type === 'class_declaration') {
214
+ const name = stmt.childForFieldName('name');
215
+ if (name && name.type === 'identifier') {
216
+ into.add(name.text);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ export async function lintFluidJs(code) {
222
+ const parser = await getJavaScriptParser();
223
+ const tree = parser.parse(code);
224
+ const root = tree.rootNode;
225
+ const bound = new Set();
226
+ for (const stmt of root.namedChildren) {
227
+ if (stmt.type === 'import_statement') {
228
+ collectImportedNames(stmt, bound);
229
+ }
230
+ }
231
+ collectTopLevelDeclaredNames(root, bound);
232
+ const missingByName = new Map();
233
+ walk(root, (n) => {
234
+ if (n.type === 'import_statement') {
235
+ return false;
236
+ }
237
+ if (n.type !== 'identifier') {
238
+ return;
239
+ }
240
+ if (!MODULE_FOR_SYMBOL.has(n.text)) {
241
+ return;
242
+ }
243
+ if (bound.has(n.text)) {
244
+ return;
245
+ }
246
+ if (!isReferenceUse(n)) {
247
+ return;
248
+ }
249
+ if (missingByName.has(n.text)) {
250
+ return;
251
+ }
252
+ missingByName.set(n.text, {
253
+ symbol: n.text,
254
+ module: MODULE_FOR_SYMBOL.get(n.text),
255
+ line: n.startPosition.row,
256
+ column: n.startPosition.column,
257
+ });
258
+ });
259
+ const sorted = Array.from(missingByName.values()).sort((a, b) => a.symbol.localeCompare(b.symbol));
260
+ const byModule = new Map();
261
+ for (const m of sorted) {
262
+ const list = byModule.get(m.module) ?? [];
263
+ list.push(m.symbol);
264
+ byModule.set(m.module, list);
265
+ }
266
+ const suggestion = Array.from(byModule.entries())
267
+ .sort(([a], [b]) => a.localeCompare(b))
268
+ .map(([mod, syms]) => `import { ${syms.join(', ')} } from "${mod}";`)
269
+ .join('\n');
270
+ return { missing: sorted, suggestion };
271
+ }
@@ -0,0 +1,24 @@
1
+ import { Router } from 'express';
2
+ export type DirtyFileEntry = {
3
+ /** Absolute, normalized path of the dirty file. */
4
+ path: string;
5
+ /** Editor-observed mtime when the file was last seen on disk, or 0 if unknown. */
6
+ lastModifiedMs: number;
7
+ };
8
+ /**
9
+ * Holds the set of files the editor currently considers dirty (unsaved
10
+ * changes). The MCP source-editing tools query this before writing so a
11
+ * remote agent never silently clobbers a user's in-flight changes.
12
+ *
13
+ * The set is volatile by design — it gets replaced wholesale on every
14
+ * `editor-dirty-state` IPC and resets when the server restarts. Editors
15
+ * resend their state on startup.
16
+ */
17
+ export declare class DirtyBufferState {
18
+ private files;
19
+ setDirtyFiles(paths: string[]): void;
20
+ list(): DirtyFileEntry[];
21
+ isDirty(absPath: string): boolean;
22
+ private readMtime;
23
+ }
24
+ export declare function createEditorRouter(state: DirtyBufferState): Router;
@@ -0,0 +1,44 @@
1
+ import fs from 'fs';
2
+ import { Router } from 'express';
3
+ import { normalizePath } from "../normalize-path.js";
4
+ /**
5
+ * Holds the set of files the editor currently considers dirty (unsaved
6
+ * changes). The MCP source-editing tools query this before writing so a
7
+ * remote agent never silently clobbers a user's in-flight changes.
8
+ *
9
+ * The set is volatile by design — it gets replaced wholesale on every
10
+ * `editor-dirty-state` IPC and resets when the server restarts. Editors
11
+ * resend their state on startup.
12
+ */
13
+ export class DirtyBufferState {
14
+ files = new Map();
15
+ setDirtyFiles(paths) {
16
+ const next = new Map();
17
+ for (const p of paths) {
18
+ const normalized = normalizePath(p);
19
+ next.set(normalized, this.readMtime(normalized));
20
+ }
21
+ this.files = next;
22
+ }
23
+ list() {
24
+ return Array.from(this.files, ([path, lastModifiedMs]) => ({ path, lastModifiedMs }));
25
+ }
26
+ isDirty(absPath) {
27
+ return this.files.has(normalizePath(absPath));
28
+ }
29
+ readMtime(absPath) {
30
+ try {
31
+ return fs.statSync(absPath).mtimeMs;
32
+ }
33
+ catch {
34
+ return 0;
35
+ }
36
+ }
37
+ }
38
+ export function createEditorRouter(state) {
39
+ const router = Router();
40
+ router.get('/editor/dirty-files', (_req, res) => {
41
+ res.json(state.list());
42
+ });
43
+ return router;
44
+ }
@@ -1,3 +1,3 @@
1
1
  import { Router } from 'express';
2
2
  import type { FluidCadServer } from '../fluidcad-server.ts';
3
- export declare function createExportRouter(fluidCadServer: FluidCadServer): Router;
3
+ export declare function createExportRouter(fluidCadServer: FluidCadServer, workspacePath: string): Router;
@@ -1,8 +1,20 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
1
3
  import { Router } from 'express';
2
- export function createExportRouter(fluidCadServer) {
4
+ export function createExportRouter(fluidCadServer, workspacePath) {
3
5
  const router = Router();
6
+ // Resolve the workspace root once so symlink checks below match the path
7
+ // we actually allow writes into.
8
+ const workspaceRoot = (() => {
9
+ try {
10
+ return fs.realpathSync(workspacePath);
11
+ }
12
+ catch {
13
+ return path.resolve(workspacePath);
14
+ }
15
+ })();
4
16
  router.post('/export', (req, res) => {
5
- const { format, shapeIds, includeColors, resolution, customAngularDeflectionDeg, customLinearDeflection } = req.body;
17
+ const { format, shapeIds, includeColors, resolution, customAngularDeflectionDeg, customLinearDeflection, saveAsPath } = req.body;
6
18
  if (format !== 'step' && format !== 'stl') {
7
19
  res.status(400).json({ error: 'Invalid format. Must be "step" or "stl".' });
8
20
  return;
@@ -24,6 +36,10 @@ export function createExportRouter(fluidCadServer) {
24
36
  }
25
37
  }
26
38
  }
39
+ if (saveAsPath !== undefined && typeof saveAsPath !== 'string') {
40
+ res.status(400).json({ error: 'saveAsPath must be a string.' });
41
+ return;
42
+ }
27
43
  try {
28
44
  const result = fluidCadServer.exportShapes(shapeIds, {
29
45
  format,
@@ -38,14 +54,35 @@ export function createExportRouter(fluidCadServer) {
38
54
  }
39
55
  const ext = format === 'step' ? '.step' : '.stl';
40
56
  const mimeType = format === 'step' ? 'application/step' : 'application/sla';
57
+ const bytes = typeof result.data === 'string'
58
+ ? Buffer.from(result.data, 'utf-8')
59
+ : Buffer.from(result.data);
60
+ if (saveAsPath) {
61
+ // Resolve against the workspace root, then verify the canonical path
62
+ // (after symlink resolution of the parent) still lives inside it.
63
+ const candidate = path.resolve(workspaceRoot, saveAsPath);
64
+ const parent = path.dirname(candidate);
65
+ let parentReal;
66
+ try {
67
+ parentReal = fs.realpathSync(parent);
68
+ }
69
+ catch {
70
+ res.status(400).json({ error: `Parent directory does not exist: ${parent}` });
71
+ return;
72
+ }
73
+ const canonical = path.join(parentReal, path.basename(candidate));
74
+ const rel = path.relative(workspaceRoot, canonical);
75
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
76
+ res.status(400).json({ error: `saveAsPath escapes workspace root: ${saveAsPath}` });
77
+ return;
78
+ }
79
+ fs.writeFileSync(canonical, bytes);
80
+ res.json({ savedTo: canonical, bytesWritten: bytes.length });
81
+ return;
82
+ }
41
83
  res.setHeader('Content-Type', mimeType);
42
84
  res.setHeader('Content-Disposition', `attachment; filename="export${ext}"`);
43
- if (typeof result.data === 'string') {
44
- res.send(Buffer.from(result.data, 'utf-8'));
45
- }
46
- else {
47
- res.send(Buffer.from(result.data));
48
- }
85
+ res.send(bytes);
49
86
  }
50
87
  catch (err) {
51
88
  res.status(500).json({ error: err.message || String(err) });
@@ -0,0 +1,7 @@
1
+ import { Router } from 'express';
2
+ export type HealthInfo = {
3
+ version: string;
4
+ workspacePath: string;
5
+ startedAt: string;
6
+ };
7
+ export declare function createHealthRouter(info: HealthInfo): Router;
@@ -0,0 +1,14 @@
1
+ import { Router } from 'express';
2
+ export function createHealthRouter(info) {
3
+ const router = Router();
4
+ router.get('/health', (_req, res) => {
5
+ res.json({
6
+ ok: true,
7
+ version: info.version,
8
+ workspacePath: info.workspacePath,
9
+ startedAt: info.startedAt,
10
+ pid: process.pid,
11
+ });
12
+ });
13
+ return router;
14
+ }
@@ -0,0 +1,10 @@
1
+ import { Router } from 'express';
2
+ /**
3
+ * `POST /api/lint-fluid-js` — static import lint for a `.fluid.js` payload.
4
+ * Used by the MCP server before it writes a file so the agent learns to add
5
+ * imports on retry instead of failing at runtime with `ReferenceError`.
6
+ *
7
+ * Request body: `{ code: string }`
8
+ * Response: `{ missing: MissingImport[], suggestion: string }`
9
+ */
10
+ export declare function createLintRouter(): Router;
@@ -0,0 +1,28 @@
1
+ import { Router } from 'express';
2
+ import { lintFluidJs } from "../lint-fluid-js.js";
3
+ /**
4
+ * `POST /api/lint-fluid-js` — static import lint for a `.fluid.js` payload.
5
+ * Used by the MCP server before it writes a file so the agent learns to add
6
+ * imports on retry instead of failing at runtime with `ReferenceError`.
7
+ *
8
+ * Request body: `{ code: string }`
9
+ * Response: `{ missing: MissingImport[], suggestion: string }`
10
+ */
11
+ export function createLintRouter() {
12
+ const router = Router();
13
+ router.post('/lint-fluid-js', async (req, res) => {
14
+ const { code } = req.body ?? {};
15
+ if (typeof code !== 'string') {
16
+ res.status(400).json({ error: '`code` must be a string.' });
17
+ return;
18
+ }
19
+ try {
20
+ const result = await lintFluidJs(code);
21
+ res.json(result);
22
+ }
23
+ catch (err) {
24
+ res.status(500).json({ error: err?.message ?? String(err) });
25
+ }
26
+ });
27
+ return router;
28
+ }
@@ -0,0 +1,33 @@
1
+ import { Router } from 'express';
2
+ import type { CompileError } from '../ws-protocol.ts';
3
+ export type RenderOutcome = {
4
+ state: 'rendered';
5
+ version: number;
6
+ absPath: string;
7
+ durationMs: number;
8
+ } | {
9
+ state: 'compile-error';
10
+ version: number;
11
+ durationMs: number;
12
+ compileError: CompileError;
13
+ } | {
14
+ state: 'superseded';
15
+ version: number;
16
+ durationMs: number;
17
+ } | {
18
+ state: 'no-scene-manager';
19
+ version: number;
20
+ durationMs: number;
21
+ };
22
+ /**
23
+ * `POST /api/render` — synchronous render trigger used by the MCP server
24
+ * after it writes a `.fluid.js` file. The body carries the post-write
25
+ * contents so the server doesn't need to re-read disk and so dedup against
26
+ * `lastRendered` is exact. Returns the render outcome (rendered / compile-
27
+ * error / superseded / no-scene-manager) once the OCC pass settles.
28
+ *
29
+ * Whoever invokes this is responsible for the on-disk write — we only run
30
+ * the render. Pairing both in one HTTP round-trip is what lets MCP
31
+ * `write_file` return a synchronous { written, render } to the agent.
32
+ */
33
+ export declare function createRenderRouter(runLiveRender: (fileName: string, code: string) => Promise<RenderOutcome>): Router;
@@ -0,0 +1,34 @@
1
+ import { Router } from 'express';
2
+ /**
3
+ * `POST /api/render` — synchronous render trigger used by the MCP server
4
+ * after it writes a `.fluid.js` file. The body carries the post-write
5
+ * contents so the server doesn't need to re-read disk and so dedup against
6
+ * `lastRendered` is exact. Returns the render outcome (rendered / compile-
7
+ * error / superseded / no-scene-manager) once the OCC pass settles.
8
+ *
9
+ * Whoever invokes this is responsible for the on-disk write — we only run
10
+ * the render. Pairing both in one HTTP round-trip is what lets MCP
11
+ * `write_file` return a synchronous { written, render } to the agent.
12
+ */
13
+ export function createRenderRouter(runLiveRender) {
14
+ const router = Router();
15
+ router.post('/render', async (req, res) => {
16
+ const { filePath, code } = req.body ?? {};
17
+ if (typeof filePath !== 'string' || filePath.length === 0) {
18
+ res.status(400).json({ error: '`filePath` must be a non-empty string.' });
19
+ return;
20
+ }
21
+ if (typeof code !== 'string') {
22
+ res.status(400).json({ error: '`code` must be a string.' });
23
+ return;
24
+ }
25
+ try {
26
+ const outcome = await runLiveRender(filePath, code);
27
+ res.json(outcome);
28
+ }
29
+ catch (err) {
30
+ res.status(500).json({ error: err?.message ?? String(err) });
31
+ }
32
+ });
33
+ return router;
34
+ }
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import type { FluidCadServer } from '../fluidcad-server.ts';
3
+ import type { CameraStateMessage } from '../ws-protocol.ts';
4
+ export type CameraStateGetter = () => CameraStateMessage | null;
5
+ export declare function createSceneRouter(fluidCadServer: FluidCadServer, getCameraState?: CameraStateGetter): Router;
@@ -0,0 +1,48 @@
1
+ import { Router } from 'express';
2
+ export function createSceneRouter(fluidCadServer, getCameraState = () => null) {
3
+ const router = Router();
4
+ router.get('/scene/summary', (_req, res) => {
5
+ try {
6
+ const summary = fluidCadServer.getSceneSummary();
7
+ if (!summary) {
8
+ res.status(404).json({ error: 'No scene available — the workspace has not rendered a file yet.' });
9
+ return;
10
+ }
11
+ res.json(summary);
12
+ }
13
+ catch (err) {
14
+ res.status(500).json({ error: err?.message ?? String(err) });
15
+ }
16
+ });
17
+ router.get('/scene/shapes', (_req, res) => {
18
+ try {
19
+ const shapes = fluidCadServer.getShapesList();
20
+ if (!shapes) {
21
+ res.status(404).json({ error: 'No scene available — the workspace has not rendered a file yet.' });
22
+ return;
23
+ }
24
+ res.json(shapes);
25
+ }
26
+ catch (err) {
27
+ res.status(500).json({ error: err?.message ?? String(err) });
28
+ }
29
+ });
30
+ router.get('/scene/compile-error', (_req, res) => {
31
+ try {
32
+ const compileError = fluidCadServer.getCompileError();
33
+ res.json({ compileError });
34
+ }
35
+ catch (err) {
36
+ res.status(500).json({ error: err?.message ?? String(err) });
37
+ }
38
+ });
39
+ router.get('/camera/state', (_req, res) => {
40
+ const state = getCameraState();
41
+ if (!state) {
42
+ res.status(404).json({ error: 'No camera state available yet — the UI has not connected, or no view change has been observed.' });
43
+ return;
44
+ }
45
+ res.json(state);
46
+ });
47
+ return router;
48
+ }