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,64 @@
1
+ import { WebSocket } from 'ws';
2
+ import type { RegistryEntry } from './types.ts';
3
+ /** Lifecycle ping for one render pass on the server. */
4
+ export type RenderEvent = {
5
+ version: number;
6
+ state: 'start' | 'end' | 'error';
7
+ absPath?: string;
8
+ /** Local monotonic timestamp when the message was received. */
9
+ receivedAt: number;
10
+ };
11
+ export type IdleResult = {
12
+ idleMs: number;
13
+ lastVersion: number | null;
14
+ };
15
+ export type HealthResponse = {
16
+ ok: boolean;
17
+ version: string;
18
+ workspacePath: string;
19
+ startedAt: string;
20
+ pid: number;
21
+ };
22
+ export declare class FluidCadClient {
23
+ readonly entry: RegistryEntry;
24
+ private readonly origin;
25
+ private readonly pool;
26
+ private ws;
27
+ private wsOpen;
28
+ private closed;
29
+ private lastStartAt;
30
+ private lastStartVersion;
31
+ private renderListeners;
32
+ private wsErrorListeners;
33
+ constructor(entry: RegistryEntry);
34
+ /** Quick liveness check. Returns null when the server is unreachable. */
35
+ health(): Promise<HealthResponse | null>;
36
+ getJson<T>(path: string): Promise<T>;
37
+ postJson<T>(path: string, body: unknown): Promise<T>;
38
+ postRaw(path: string, body: unknown): Promise<{
39
+ statusCode: number;
40
+ data: Buffer;
41
+ contentType: string;
42
+ }>;
43
+ ensureWebSocket(): Promise<WebSocket>;
44
+ /**
45
+ * Wait until `stableMs` has passed since the last observed render `start`.
46
+ * Resolves immediately (with `idleMs = stableMs`) if no `start` has been
47
+ * seen since subscription opened.
48
+ */
49
+ nextIdle(stableMs: number, timeoutMs: number): Promise<IdleResult>;
50
+ close(): Promise<void>;
51
+ private handleWsMessage;
52
+ private notifyWsError;
53
+ }
54
+ export declare class TimeoutError extends Error {
55
+ constructor(message: string);
56
+ }
57
+ export declare class WsError extends Error {
58
+ constructor(message: string);
59
+ }
60
+ export declare class HttpError extends Error {
61
+ readonly statusCode: number;
62
+ readonly body: string;
63
+ constructor(statusCode: number, body: string);
64
+ }
@@ -0,0 +1,248 @@
1
+ // HTTP+WS client against a running FluidCadServer.
2
+ //
3
+ // Owned by the MCP process, one instance per workspace it talks to. Holds an
4
+ // undici Pool keyed by the server's origin and a lazily-opened WebSocket for
5
+ // streaming notifications. The future per-instance auth header would land
6
+ // here (and only here).
7
+ import { Pool } from 'undici';
8
+ import { WebSocket } from 'ws';
9
+ const HEALTH_PROBE_TIMEOUT_MS = 500;
10
+ export class FluidCadClient {
11
+ entry;
12
+ origin;
13
+ pool;
14
+ ws = null;
15
+ wsOpen = null;
16
+ closed = false;
17
+ // Render-event tracking. Latest `start` timestamp is kept so wait-for-idle
18
+ // can answer "stable for at least N ms" without re-subscribing each call.
19
+ // Listeners receive every render-version message.
20
+ lastStartAt = null;
21
+ lastStartVersion = null;
22
+ renderListeners = new Set();
23
+ wsErrorListeners = new Set();
24
+ constructor(entry) {
25
+ this.entry = entry;
26
+ this.origin = `http://127.0.0.1:${entry.port}`;
27
+ this.pool = new Pool(this.origin, { connections: 4 });
28
+ }
29
+ /** Quick liveness check. Returns null when the server is unreachable. */
30
+ async health() {
31
+ try {
32
+ const res = await this.pool.request({
33
+ path: '/api/health',
34
+ method: 'GET',
35
+ bodyTimeout: HEALTH_PROBE_TIMEOUT_MS,
36
+ headersTimeout: HEALTH_PROBE_TIMEOUT_MS,
37
+ });
38
+ if (res.statusCode !== 200) {
39
+ return null;
40
+ }
41
+ const body = await res.body.json();
42
+ return body;
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async getJson(path) {
49
+ const res = await this.pool.request({ path, method: 'GET' });
50
+ return readJsonBody(res);
51
+ }
52
+ async postJson(path, body) {
53
+ const res = await this.pool.request({
54
+ path,
55
+ method: 'POST',
56
+ headers: { 'content-type': 'application/json' },
57
+ body: JSON.stringify(body ?? {}),
58
+ });
59
+ return readJsonBody(res);
60
+ }
61
+ async postRaw(path, body) {
62
+ const res = await this.pool.request({
63
+ path,
64
+ method: 'POST',
65
+ headers: { 'content-type': 'application/json' },
66
+ body: JSON.stringify(body ?? {}),
67
+ });
68
+ const chunks = [];
69
+ for await (const chunk of res.body) {
70
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
71
+ }
72
+ const contentType = String(res.headers['content-type'] ?? 'application/octet-stream');
73
+ return { statusCode: res.statusCode, data: Buffer.concat(chunks), contentType };
74
+ }
75
+ async ensureWebSocket() {
76
+ if (this.closed) {
77
+ throw new Error('Client is closed.');
78
+ }
79
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
80
+ return this.ws;
81
+ }
82
+ if (this.wsOpen) {
83
+ return this.wsOpen;
84
+ }
85
+ const url = `ws://127.0.0.1:${this.entry.port}`;
86
+ const socket = new WebSocket(url);
87
+ this.ws = socket;
88
+ this.wsOpen = new Promise((resolve, reject) => {
89
+ const onOpen = () => {
90
+ cleanup();
91
+ resolve(socket);
92
+ };
93
+ const onError = (err) => {
94
+ cleanup();
95
+ this.ws = null;
96
+ this.wsOpen = null;
97
+ this.notifyWsError(err);
98
+ reject(err);
99
+ };
100
+ const cleanup = () => {
101
+ socket.off('open', onOpen);
102
+ socket.off('error', onError);
103
+ };
104
+ socket.once('open', onOpen);
105
+ socket.once('error', onError);
106
+ });
107
+ socket.on('message', (raw) => this.handleWsMessage(String(raw)));
108
+ socket.on('close', () => {
109
+ if (this.ws === socket) {
110
+ this.ws = null;
111
+ this.wsOpen = null;
112
+ }
113
+ // Surface a close as an error to anyone currently awaiting events. They
114
+ // can retry; the next call will reconnect lazily.
115
+ this.notifyWsError(new Error('WebSocket closed.'));
116
+ });
117
+ return this.wsOpen;
118
+ }
119
+ /**
120
+ * Wait until `stableMs` has passed since the last observed render `start`.
121
+ * Resolves immediately (with `idleMs = stableMs`) if no `start` has been
122
+ * seen since subscription opened.
123
+ */
124
+ async nextIdle(stableMs, timeoutMs) {
125
+ await this.ensureWebSocket();
126
+ return new Promise((resolve, reject) => {
127
+ const subscribeAt = nowMs();
128
+ let latestStartAt = this.lastStartAt;
129
+ let latestStartVersion = this.lastStartVersion;
130
+ const cleanup = () => {
131
+ clearTimeout(idleTimer);
132
+ clearTimeout(deadlineTimer);
133
+ this.renderListeners.delete(onEvent);
134
+ this.wsErrorListeners.delete(onError);
135
+ };
136
+ const settle = () => {
137
+ cleanup();
138
+ const last = latestStartAt ?? subscribeAt;
139
+ resolve({
140
+ idleMs: Math.max(0, nowMs() - last),
141
+ lastVersion: latestStartVersion,
142
+ });
143
+ };
144
+ const armIdleTimer = (since) => {
145
+ clearTimeout(idleTimer);
146
+ const remaining = Math.max(0, stableMs - (nowMs() - since));
147
+ idleTimer = setTimeout(settle, remaining);
148
+ };
149
+ let idleTimer = setTimeout(settle, stableMs);
150
+ const deadlineTimer = setTimeout(() => {
151
+ cleanup();
152
+ reject(new TimeoutError(`Not idle for ${stableMs}ms within ${timeoutMs}ms.`));
153
+ }, timeoutMs);
154
+ const onEvent = (event) => {
155
+ if (event.state !== 'start') {
156
+ return;
157
+ }
158
+ latestStartAt = event.receivedAt;
159
+ latestStartVersion = event.version;
160
+ armIdleTimer(event.receivedAt);
161
+ };
162
+ const onError = (err) => {
163
+ cleanup();
164
+ reject(new WsError(err.message));
165
+ };
166
+ this.renderListeners.add(onEvent);
167
+ this.wsErrorListeners.add(onError);
168
+ });
169
+ }
170
+ async close() {
171
+ this.closed = true;
172
+ this.renderListeners.clear();
173
+ this.wsErrorListeners.clear();
174
+ if (this.ws) {
175
+ this.ws.removeAllListeners('close');
176
+ this.ws.close();
177
+ this.ws = null;
178
+ this.wsOpen = null;
179
+ }
180
+ await this.pool.close();
181
+ }
182
+ handleWsMessage(raw) {
183
+ let msg;
184
+ try {
185
+ msg = JSON.parse(raw);
186
+ }
187
+ catch {
188
+ return;
189
+ }
190
+ if (!msg || msg.type !== 'render-version' || typeof msg.version !== 'number') {
191
+ return;
192
+ }
193
+ const state = msg.state;
194
+ if (state !== 'start' && state !== 'end' && state !== 'error') {
195
+ return;
196
+ }
197
+ const event = {
198
+ version: msg.version,
199
+ state,
200
+ absPath: typeof msg.absPath === 'string' ? msg.absPath : undefined,
201
+ receivedAt: nowMs(),
202
+ };
203
+ if (state === 'start') {
204
+ this.lastStartAt = event.receivedAt;
205
+ this.lastStartVersion = event.version;
206
+ }
207
+ for (const listener of [...this.renderListeners]) {
208
+ listener(event);
209
+ }
210
+ }
211
+ notifyWsError(err) {
212
+ for (const listener of [...this.wsErrorListeners]) {
213
+ listener(err);
214
+ }
215
+ }
216
+ }
217
+ function nowMs() {
218
+ return Number(process.hrtime.bigint() / 1000000n);
219
+ }
220
+ export class TimeoutError extends Error {
221
+ constructor(message) {
222
+ super(message);
223
+ this.name = 'TimeoutError';
224
+ }
225
+ }
226
+ export class WsError extends Error {
227
+ constructor(message) {
228
+ super(message);
229
+ this.name = 'WsError';
230
+ }
231
+ }
232
+ async function readJsonBody(res) {
233
+ if (res.statusCode >= 400) {
234
+ const text = await res.body.text();
235
+ throw new HttpError(res.statusCode, text);
236
+ }
237
+ return await res.body.json();
238
+ }
239
+ export class HttpError extends Error {
240
+ statusCode;
241
+ body;
242
+ constructor(statusCode, body) {
243
+ super(`HTTP ${statusCode}: ${body.slice(0, 200)}`);
244
+ this.statusCode = statusCode;
245
+ this.body = body;
246
+ this.name = 'HttpError';
247
+ }
248
+ }
@@ -0,0 +1,11 @@
1
+ import type { RegistryEntry } from './types.ts';
2
+ export declare function registryFilePath(): string;
3
+ /**
4
+ * Read the on-disk registry and return entries whose PID is still alive.
5
+ *
6
+ * Unlike the server-side `readInstances()`, this never writes back the pruned
7
+ * set — a read-only MCP process should not modify shared global state.
8
+ */
9
+ export declare function listLiveInstances(): RegistryEntry[];
10
+ export declare function findByWorkspace(workspacePath: string): RegistryEntry | null;
11
+ export declare function isPidAlive(pid: number): boolean;
@@ -0,0 +1,78 @@
1
+ // Read-only access to the FluidCAD instance registry.
2
+ //
3
+ // The MCP process never writes the registry — that's owned by the server.
4
+ // We re-implement just the read path here so the MCP package doesn't depend
5
+ // on the entire server package surface.
6
+ import fs from 'fs';
7
+ import os from 'os';
8
+ import path from 'path';
9
+ const REGISTRY_DIR_NAME = '.fluidcad';
10
+ const REGISTRY_FILE_NAME = 'instances.json';
11
+ export function registryFilePath() {
12
+ return path.join(os.homedir(), REGISTRY_DIR_NAME, REGISTRY_FILE_NAME);
13
+ }
14
+ /**
15
+ * Read the on-disk registry and return entries whose PID is still alive.
16
+ *
17
+ * Unlike the server-side `readInstances()`, this never writes back the pruned
18
+ * set — a read-only MCP process should not modify shared global state.
19
+ */
20
+ export function listLiveInstances() {
21
+ const file = readRegistryFile();
22
+ return file.instances.filter((entry) => isPidAlive(entry.pid));
23
+ }
24
+ export function findByWorkspace(workspacePath) {
25
+ for (const entry of listLiveInstances()) {
26
+ if (entry.workspacePath === workspacePath) {
27
+ return entry;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+ export function isPidAlive(pid) {
33
+ try {
34
+ process.kill(pid, 0);
35
+ return true;
36
+ }
37
+ catch (err) {
38
+ if (err && err.code === 'EPERM') {
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+ }
44
+ const EMPTY = { schemaVersion: 1, instances: [] };
45
+ function readRegistryFile() {
46
+ try {
47
+ const raw = fs.readFileSync(registryFilePath(), 'utf8');
48
+ const parsed = JSON.parse(raw);
49
+ if (isRegistryFile(parsed)) {
50
+ return parsed;
51
+ }
52
+ return EMPTY;
53
+ }
54
+ catch {
55
+ return EMPTY;
56
+ }
57
+ }
58
+ function isRegistryFile(value) {
59
+ if (typeof value !== 'object' || value === null) {
60
+ return false;
61
+ }
62
+ const v = value;
63
+ if (v.schemaVersion !== 1 || !Array.isArray(v.instances)) {
64
+ return false;
65
+ }
66
+ return v.instances.every(isRegistryEntry);
67
+ }
68
+ function isRegistryEntry(value) {
69
+ if (typeof value !== 'object' || value === null) {
70
+ return false;
71
+ }
72
+ const v = value;
73
+ return (typeof v.workspacePath === 'string' &&
74
+ typeof v.port === 'number' &&
75
+ typeof v.pid === 'number' &&
76
+ typeof v.version === 'string' &&
77
+ typeof v.startedAt === 'string');
78
+ }
@@ -0,0 +1,81 @@
1
+ export type DocRecord = {
2
+ id: string;
3
+ title: string;
4
+ summary: string;
5
+ tags?: string[];
6
+ symbols?: string[];
7
+ seeAlso?: string[];
8
+ file: string;
9
+ bodyLength: number;
10
+ };
11
+ type IndexFile = {
12
+ schemaVersion: 1;
13
+ generatedAt: string;
14
+ docs: DocRecord[];
15
+ };
16
+ type ApiIndexFile = {
17
+ schemaVersion: 1;
18
+ generatedAt: string;
19
+ symbols: Record<string, string>;
20
+ };
21
+ export type SearchHit = {
22
+ id: string;
23
+ title: string;
24
+ snippet: string;
25
+ score: number;
26
+ };
27
+ export declare class DocsIndex {
28
+ /** Absolute path to the docs root directory. */
29
+ readonly root: string;
30
+ /** Every doc record, in the order they were emitted by the build script. */
31
+ readonly docs: ReadonlyArray<DocRecord>;
32
+ /** Symbol -> docId (from `llm-docs/api/index.json`). */
33
+ readonly symbols: Readonly<Record<string, string>>;
34
+ /**
35
+ * Type-name -> docId, restricted to docs under `api/types/`. Populated by
36
+ * filtering `symbols` so the same symbol map drives both `get_api_signature`
37
+ * and `get_type_definition`, but the latter rejects function symbols with a
38
+ * clear "not a type" error rather than returning a feature signature.
39
+ */
40
+ readonly types: Readonly<Record<string, string>>;
41
+ private readonly byId;
42
+ private readonly bodyCache;
43
+ /** token -> docId -> per-field hit counts. */
44
+ private readonly inverted;
45
+ constructor(root: string, index: IndexFile, apiIndex: ApiIndexFile);
46
+ /** All docs, optionally filtered to those carrying a given tag. */
47
+ list(tag?: string): DocRecord[];
48
+ get(id: string): DocRecord | null;
49
+ /**
50
+ * Load the markdown body for a doc id, stripping the YAML frontmatter so
51
+ * callers see the same text the docs author wrote (no metadata clutter).
52
+ */
53
+ body(id: string): string | null;
54
+ /** Keyword search. Returns ranked hits with an ~80-char snippet per result. */
55
+ search(query: string, limit?: number): SearchHit[];
56
+ /** First fenced code block in the body, used as a symbol's signature surface. */
57
+ firstCodeBlock(id: string): string | null;
58
+ private buildInverted;
59
+ private snippet;
60
+ }
61
+ /**
62
+ * Locate the on-disk `llm-docs/` directory.
63
+ *
64
+ * Resolution order:
65
+ * 1. The installed package — `require.resolve('fluidcad/package.json')` gives
66
+ * us the consumer's `node_modules/fluidcad/`, and `llm-docs/` sits next to
67
+ * that `package.json`.
68
+ * 2. The in-repo path — walk up from this file (`mcp/src/` or `mcp/dist/`)
69
+ * until we hit a sibling `llm-docs/` directory.
70
+ *
71
+ * We never trust `process.cwd()`: the MCP process is launched by an external
72
+ * client (Claude Desktop, etc.) so its CWD is whatever they happen to be in.
73
+ */
74
+ export declare function resolveDocsRoot(): string;
75
+ /**
76
+ * Load the manifests from `<root>/index.json` and `<root>/api/index.json` and
77
+ * construct the in-memory index. Throws (with a helpful message) if either
78
+ * manifest is missing — that almost always means `build:llm-docs` was skipped.
79
+ */
80
+ export declare function loadDocsIndex(root?: string): DocsIndex;
81
+ export {};