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.
- package/README.md +3 -2
- package/bin/commands/init.js +55 -0
- package/bin/commands/mcp.js +33 -0
- package/bin/commands/serve.js +77 -0
- package/bin/fluidcad.js +15 -107
- package/lib/dist/common/scene-object.d.ts +4 -1
- package/lib/dist/common/scene-object.js +9 -2
- package/lib/dist/common/solid.d.ts +4 -1
- package/lib/dist/common/solid.js +13 -0
- package/lib/dist/core/2d/tarc.d.ts +20 -2
- package/lib/dist/core/2d/tarc.js +24 -0
- package/lib/dist/core/index.d.ts +2 -1
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +107 -2
- package/lib/dist/core/load.d.ts +2 -2
- package/lib/dist/core/repeat.js +62 -46
- package/lib/dist/core/rib.d.ts +18 -0
- package/lib/dist/core/rib.js +37 -0
- package/lib/dist/features/2d/arc.d.ts +8 -2
- package/lib/dist/features/2d/arc.js +94 -17
- package/lib/dist/features/2d/back.js +3 -2
- package/lib/dist/features/2d/sketch.d.ts +4 -0
- package/lib/dist/features/2d/sketch.js +21 -0
- package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
- package/lib/dist/features/2d/tarc-constrained.js +8 -0
- package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
- package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
- package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
- package/lib/dist/features/2d/tarc-to-object.js +66 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
- package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
- package/lib/dist/features/2d/tarc-to-point.js +3 -0
- package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
- package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
- package/lib/dist/features/2d/tarc.d.ts +2 -0
- package/lib/dist/features/2d/tarc.js +3 -0
- package/lib/dist/features/extrude-base.d.ts +9 -0
- package/lib/dist/features/extrude-base.js +22 -0
- package/lib/dist/features/extrude-to-face.js +1 -5
- package/lib/dist/features/extrude-two-distances.js +1 -2
- package/lib/dist/features/extrude.js +1 -2
- package/lib/dist/features/load.d.ts +6 -0
- package/lib/dist/features/load.js +53 -1
- package/lib/dist/features/mirror-feature.d.ts +3 -2
- package/lib/dist/features/mirror-feature.js +1 -1
- package/lib/dist/features/repeat-circular.d.ts +3 -3
- package/lib/dist/features/repeat-circular.js +8 -1
- package/lib/dist/features/repeat-linear.d.ts +4 -2
- package/lib/dist/features/repeat-linear.js +10 -1
- package/lib/dist/features/repeat-matrix.d.ts +3 -1
- package/lib/dist/features/repeat-matrix.js +7 -2
- package/lib/dist/features/rib.d.ts +31 -0
- package/lib/dist/features/rib.js +321 -0
- package/lib/dist/features/select.d.ts +1 -0
- package/lib/dist/features/select.js +81 -10
- package/lib/dist/features/shell.d.ts +4 -1
- package/lib/dist/features/shell.js +14 -3
- package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
- package/lib/dist/filters/edge/belongs-to-face.js +64 -15
- package/lib/dist/filters/filter-builder-base.d.ts +25 -0
- package/lib/dist/filters/filter-builder-base.js +47 -0
- package/lib/dist/filters/filter.js +39 -14
- package/lib/dist/filters/from-object.d.ts +4 -0
- package/lib/dist/filters/from-object.js +10 -0
- package/lib/dist/helpers/clone-transform.d.ts +2 -1
- package/lib/dist/helpers/scene-helpers.d.ts +1 -1
- package/lib/dist/helpers/scene-helpers.js +146 -12
- package/lib/dist/index.d.ts +7 -1
- package/lib/dist/index.js +3 -3
- package/lib/dist/io/file-import.d.ts +5 -1
- package/lib/dist/io/file-import.js +29 -18
- package/lib/dist/math/lazy-matrix.d.ts +31 -0
- package/lib/dist/math/lazy-matrix.js +66 -0
- package/lib/dist/oc/color-transfer.d.ts +19 -8
- package/lib/dist/oc/color-transfer.js +70 -12
- package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
- package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
- package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
- package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
- package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
- package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
- package/lib/dist/oc/extrude-ops.d.ts +2 -1
- package/lib/dist/oc/extrude-ops.js +51 -2
- package/lib/dist/oc/mesh.d.ts +9 -4
- package/lib/dist/oc/mesh.js +14 -13
- package/lib/dist/oc/rib-ops.d.ts +35 -0
- package/lib/dist/oc/rib-ops.js +619 -0
- package/lib/dist/oc/shell-ops.d.ts +2 -1
- package/lib/dist/oc/shell-ops.js +5 -2
- package/lib/dist/oc/topology-index.d.ts +6 -0
- package/lib/dist/oc/topology-index.js +36 -0
- package/lib/dist/rendering/mesh-builder.d.ts +3 -0
- package/lib/dist/rendering/mesh-builder.js +8 -4
- package/lib/dist/rendering/render-edge.d.ts +2 -1
- package/lib/dist/rendering/render-edge.js +2 -2
- package/lib/dist/rendering/render-face.d.ts +2 -1
- package/lib/dist/rendering/render-face.js +2 -2
- package/lib/dist/rendering/render-solid.d.ts +2 -1
- package/lib/dist/rendering/render-solid.js +3 -5
- package/lib/dist/rendering/render-wire.d.ts +2 -1
- package/lib/dist/rendering/render-wire.js +2 -2
- package/lib/dist/rendering/render.d.ts +4 -0
- package/lib/dist/rendering/render.js +50 -2
- package/lib/dist/rendering/scene-compare.js +3 -0
- package/lib/dist/rendering/scene.d.ts +1 -0
- package/lib/dist/rendering/scene.js +4 -0
- package/lib/dist/scene-manager.d.ts +4 -2
- package/lib/dist/scene-manager.js +12 -4
- package/lib/dist/tests/features/2d/arc.test.js +64 -0
- package/lib/dist/tests/features/2d/back.test.js +17 -1
- package/lib/dist/tests/features/2d/tarc.test.js +157 -0
- package/lib/dist/tests/features/color-lineage.test.js +18 -0
- package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
- package/lib/dist/tests/features/filter-positional.test.js +129 -0
- package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
- package/lib/dist/tests/features/rib.test.d.ts +1 -0
- package/lib/dist/tests/features/rib.test.js +598 -0
- package/lib/dist/tests/features/shell.test.js +36 -0
- package/lib/dist/tests/global-setup.js +2 -1
- package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
- package/lib/dist/tests/helpers/extract-blocks.js +56 -0
- package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
- package/lib/dist/tests/llm-docs-examples.test.js +62 -0
- package/lib/dist/tests/scene-compare.test.d.ts +1 -0
- package/lib/dist/tests/scene-compare.test.js +77 -0
- package/lib/dist/tests/setup.js +2 -1
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/.coverage-allowlist.txt +9 -0
- package/llm-docs/api/arc.md +48 -0
- package/llm-docs/api/axis.md +42 -0
- package/llm-docs/api/bezier.md +42 -0
- package/llm-docs/api/booleans.md +44 -0
- package/llm-docs/api/chamfer.md +40 -0
- package/llm-docs/api/circle.md +36 -0
- package/llm-docs/api/color.md +34 -0
- package/llm-docs/api/connect.md +41 -0
- package/llm-docs/api/constraint-qualifiers.md +48 -0
- package/llm-docs/api/copy.md +63 -0
- package/llm-docs/api/cursor-lines.md +50 -0
- package/llm-docs/api/cursor-move.md +61 -0
- package/llm-docs/api/cut.md +55 -0
- package/llm-docs/api/draft.md +36 -0
- package/llm-docs/api/edge-filter.md +57 -0
- package/llm-docs/api/ellipse.md +34 -0
- package/llm-docs/api/extrude.md +74 -0
- package/llm-docs/api/face-filter.md +61 -0
- package/llm-docs/api/fillet.md +51 -0
- package/llm-docs/api/index.json +139 -0
- package/llm-docs/api/line.md +42 -0
- package/llm-docs/api/load.md +37 -0
- package/llm-docs/api/local.md +38 -0
- package/llm-docs/api/loft.md +37 -0
- package/llm-docs/api/mirror.md +44 -0
- package/llm-docs/api/offset.md +36 -0
- package/llm-docs/api/part.md +40 -0
- package/llm-docs/api/plane.md +44 -0
- package/llm-docs/api/polygon.md +37 -0
- package/llm-docs/api/primitive-solids.md +39 -0
- package/llm-docs/api/project-intersect.md +48 -0
- package/llm-docs/api/rect.md +48 -0
- package/llm-docs/api/remove.md +32 -0
- package/llm-docs/api/repeat.md +79 -0
- package/llm-docs/api/revolve.md +38 -0
- package/llm-docs/api/rib.md +40 -0
- package/llm-docs/api/rotate.md +37 -0
- package/llm-docs/api/select.md +41 -0
- package/llm-docs/api/shell.md +41 -0
- package/llm-docs/api/sketch.md +76 -0
- package/llm-docs/api/slot.md +36 -0
- package/llm-docs/api/split-trim.md +42 -0
- package/llm-docs/api/sweep.md +43 -0
- package/llm-docs/api/tarc.md +45 -0
- package/llm-docs/api/tcircle.md +38 -0
- package/llm-docs/api/tline.md +42 -0
- package/llm-docs/api/translate.md +40 -0
- package/llm-docs/api/types/aline.md +35 -0
- package/llm-docs/api/types/arc-angles.md +29 -0
- package/llm-docs/api/types/arc-points.md +48 -0
- package/llm-docs/api/types/axis-like.md +38 -0
- package/llm-docs/api/types/axis.md +21 -0
- package/llm-docs/api/types/boolean-operation.md +50 -0
- package/llm-docs/api/types/circular-repeat-options.md +31 -0
- package/llm-docs/api/types/common.md +32 -0
- package/llm-docs/api/types/cut.md +125 -0
- package/llm-docs/api/types/draft.md +21 -0
- package/llm-docs/api/types/extrudable-geometry.md +23 -0
- package/llm-docs/api/types/extrude.md +194 -0
- package/llm-docs/api/types/geometry.md +51 -0
- package/llm-docs/api/types/hline.md +35 -0
- package/llm-docs/api/types/linear-repeat-options.md +31 -0
- package/llm-docs/api/types/loft.md +154 -0
- package/llm-docs/api/types/mirror.md +35 -0
- package/llm-docs/api/types/offset.md +31 -0
- package/llm-docs/api/types/plane-like.md +35 -0
- package/llm-docs/api/types/plane-transform-options.md +29 -0
- package/llm-docs/api/types/plane.md +21 -0
- package/llm-docs/api/types/point-like.md +22 -0
- package/llm-docs/api/types/point2dlike.md +26 -0
- package/llm-docs/api/types/polygon.md +46 -0
- package/llm-docs/api/types/rect.md +128 -0
- package/llm-docs/api/types/revolve.md +102 -0
- package/llm-docs/api/types/rib.md +133 -0
- package/llm-docs/api/types/scene-object.md +33 -0
- package/llm-docs/api/types/select.md +21 -0
- package/llm-docs/api/types/shell.md +54 -0
- package/llm-docs/api/types/slot.md +43 -0
- package/llm-docs/api/types/sweep.md +189 -0
- package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
- package/llm-docs/api/types/transformable.md +93 -0
- package/llm-docs/api/types/trim.md +27 -0
- package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
- package/llm-docs/api/types/vertex.md +17 -0
- package/llm-docs/api/types/vline.md +35 -0
- package/llm-docs/concepts/coordinate-system.md +45 -0
- package/llm-docs/concepts/history-and-rollback.md +40 -0
- package/llm-docs/concepts/last-selection.md +49 -0
- package/llm-docs/concepts/scene-graph.md +37 -0
- package/llm-docs/index.json +1750 -0
- package/mcp/dist/client.d.ts +64 -0
- package/mcp/dist/client.js +248 -0
- package/mcp/dist/discovery.d.ts +11 -0
- package/mcp/dist/discovery.js +78 -0
- package/mcp/dist/docs-index.d.ts +81 -0
- package/mcp/dist/docs-index.js +261 -0
- package/mcp/dist/resources.d.ts +4 -0
- package/mcp/dist/resources.js +115 -0
- package/mcp/dist/server.d.ts +12 -0
- package/mcp/dist/server.js +489 -0
- package/mcp/dist/tools/coordination.d.ts +9 -0
- package/mcp/dist/tools/coordination.js +46 -0
- package/mcp/dist/tools/docs.d.ts +66 -0
- package/mcp/dist/tools/docs.js +122 -0
- package/mcp/dist/tools/engine.d.ts +56 -0
- package/mcp/dist/tools/engine.js +145 -0
- package/mcp/dist/tools/inspection.d.ts +75 -0
- package/mcp/dist/tools/inspection.js +121 -0
- package/mcp/dist/tools/screenshot.d.ts +63 -0
- package/mcp/dist/tools/screenshot.js +263 -0
- package/mcp/dist/tools/source.d.ts +84 -0
- package/mcp/dist/tools/source.js +434 -0
- package/mcp/dist/tools/workspaces.d.ts +13 -0
- package/mcp/dist/tools/workspaces.js +33 -0
- package/mcp/dist/types.d.ts +18 -0
- package/mcp/dist/types.js +11 -0
- package/package.json +19 -5
- package/server/dist/code-editor.d.ts +36 -0
- package/server/dist/code-editor.js +8 -0
- package/server/dist/fluidcad-server.d.ts +50 -0
- package/server/dist/fluidcad-server.js +153 -1
- package/server/dist/global-registry.d.ts +30 -0
- package/server/dist/global-registry.js +126 -0
- package/server/dist/index.js +171 -26
- package/server/dist/instance-file.d.ts +31 -0
- package/server/dist/instance-file.js +73 -0
- package/server/dist/lint-fluid-js.d.ts +15 -0
- package/server/dist/lint-fluid-js.js +271 -0
- package/server/dist/routes/editor.d.ts +24 -0
- package/server/dist/routes/editor.js +44 -0
- package/server/dist/routes/export.d.ts +1 -1
- package/server/dist/routes/export.js +45 -8
- package/server/dist/routes/health.d.ts +7 -0
- package/server/dist/routes/health.js +14 -0
- package/server/dist/routes/lint.d.ts +10 -0
- package/server/dist/routes/lint.js +28 -0
- package/server/dist/routes/render.d.ts +33 -0
- package/server/dist/routes/render.js +34 -0
- package/server/dist/routes/scene.d.ts +5 -0
- package/server/dist/routes/scene.js +48 -0
- package/server/dist/routes/screenshot.js +68 -1
- package/server/dist/ws-protocol.d.ts +56 -2
- package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
- package/ui/dist/index.html +1 -1
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// Source-editing tools — let the agent read, write, and walk `.fluid.js`
|
|
2
|
+
// sources inside a workspace. Writes are gated by a dirty-buffer check
|
|
3
|
+
// against the editor extension, atomic via tmp+rename, and confined to the
|
|
4
|
+
// workspace root (symlinks that escape are rejected).
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import fsp from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { findByWorkspace, listLiveInstances } from "../discovery.js";
|
|
9
|
+
import { FluidCadClient, HttpError } from "../client.js";
|
|
10
|
+
import { err, ok } from "../types.js";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a workspace argument to a `RegistryEntry`. Distinct from the
|
|
13
|
+
* inspection-tools variant: source tools rarely need an HTTP client (only
|
|
14
|
+
* `write_file`/`edit_range` do, and only to probe dirty buffers), so we
|
|
15
|
+
* return the entry directly and let callers open a client when required.
|
|
16
|
+
*/
|
|
17
|
+
function resolveEntry(input) {
|
|
18
|
+
if (input?.workspace) {
|
|
19
|
+
const entry = findByWorkspace(input.workspace);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
return err('workspace-not-found', `No running FluidCAD workspace at "${input.workspace}". Call list_workspaces to see what's available.`);
|
|
22
|
+
}
|
|
23
|
+
return ok(entry);
|
|
24
|
+
}
|
|
25
|
+
const instances = listLiveInstances();
|
|
26
|
+
if (instances.length === 0) {
|
|
27
|
+
return err('no-server', 'No running FluidCAD workspaces. Start one with `fluidcad serve`.');
|
|
28
|
+
}
|
|
29
|
+
if (instances.length > 1) {
|
|
30
|
+
return err('no-workspace', `Multiple FluidCAD workspaces are running (${instances.length}). Pass \`workspace\` to disambiguate.`, { workspaces: instances.map((e) => e.workspacePath) });
|
|
31
|
+
}
|
|
32
|
+
return ok(instances[0]);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a user-supplied path against the workspace root, then verify the
|
|
36
|
+
* result still lives under the workspace (symlinks included). Returns the
|
|
37
|
+
* canonical absolute path on success.
|
|
38
|
+
*/
|
|
39
|
+
function resolveWithinWorkspace(workspaceRoot, userPath, { mustExist }) {
|
|
40
|
+
if (typeof userPath !== 'string' || userPath.length === 0) {
|
|
41
|
+
return err('invalid-input', '`path` is required and must be a non-empty string.');
|
|
42
|
+
}
|
|
43
|
+
const rootReal = (() => {
|
|
44
|
+
try {
|
|
45
|
+
return fs.realpathSync(workspaceRoot);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return path.resolve(workspaceRoot);
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
const candidate = path.resolve(rootReal, userPath);
|
|
52
|
+
let canonical = candidate;
|
|
53
|
+
try {
|
|
54
|
+
canonical = fs.realpathSync(candidate);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
if (mustExist || e?.code !== 'ENOENT') {
|
|
58
|
+
if (mustExist) {
|
|
59
|
+
return err('invalid-input', `File not found: ${userPath}`);
|
|
60
|
+
}
|
|
61
|
+
return err('internal', e?.message ?? String(e));
|
|
62
|
+
}
|
|
63
|
+
// File doesn't exist yet — for writes, fall back to the parent dir's
|
|
64
|
+
// realpath so we still catch symlink escape via the parent.
|
|
65
|
+
const parent = path.dirname(candidate);
|
|
66
|
+
try {
|
|
67
|
+
const parentReal = fs.realpathSync(parent);
|
|
68
|
+
canonical = path.join(parentReal, path.basename(candidate));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Parent doesn't exist either — keep the resolved candidate; the
|
|
72
|
+
// boundary check below still catches `..` escapes.
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const rel = path.relative(rootReal, canonical);
|
|
76
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
77
|
+
return err('invalid-input', `Path escapes workspace root: ${userPath}`);
|
|
78
|
+
}
|
|
79
|
+
return ok({ absPath: canonical, rootReal });
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Static import lint for a `.fluid.js` payload via the server's
|
|
83
|
+
* `POST /api/lint-fluid-js` endpoint. The server owns the tree-sitter parser
|
|
84
|
+
* and the FluidCAD symbol table, so MCP stays a thin proxy.
|
|
85
|
+
*
|
|
86
|
+
* Failure is non-fatal: if the server endpoint is missing (older release) or
|
|
87
|
+
* the request errors, the lint is treated as "no missing imports" so writes
|
|
88
|
+
* still succeed. The render step downstream will catch any resulting
|
|
89
|
+
* `ReferenceError` if the lint was bypassed.
|
|
90
|
+
*/
|
|
91
|
+
async function lintFluidJsCode(entry, code) {
|
|
92
|
+
const client = new FluidCadClient(entry);
|
|
93
|
+
try {
|
|
94
|
+
const result = await client.postJson('/api/lint-fluid-js', { code });
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
if (e instanceof HttpError && e.statusCode === 404) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await client.close().catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function isFluidJsPath(absPath) {
|
|
108
|
+
return absPath.toLowerCase().endsWith('.fluid.js');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Refuse a write if the post-edit content uses FluidCAD APIs without the
|
|
112
|
+
* matching `import { … } from "fluidcad/…"` line. Bypassable with
|
|
113
|
+
* `force: true`, identical to the dirty-buffer guard, since both protect
|
|
114
|
+
* against the most common "first draft" mistakes LLMs make.
|
|
115
|
+
*/
|
|
116
|
+
async function assertImportsPresent(entry, absPath, code, force) {
|
|
117
|
+
if (force === true) {
|
|
118
|
+
return ok(undefined);
|
|
119
|
+
}
|
|
120
|
+
if (!isFluidJsPath(absPath)) {
|
|
121
|
+
return ok(undefined);
|
|
122
|
+
}
|
|
123
|
+
const lint = await lintFluidJsCode(entry, code);
|
|
124
|
+
if (!lint || lint.missing.length === 0) {
|
|
125
|
+
return ok(undefined);
|
|
126
|
+
}
|
|
127
|
+
const symbolList = lint.missing.map((m) => m.symbol).join(', ');
|
|
128
|
+
return err('missing-imports', [
|
|
129
|
+
`Refusing to write "${absPath}" — uses ${lint.missing.length} FluidCAD ` +
|
|
130
|
+
`symbol(s) without an import: ${symbolList}.`,
|
|
131
|
+
'Add this to the top of the file (pass `force: true` to override):',
|
|
132
|
+
lint.suggestion,
|
|
133
|
+
].join('\n'), { missing: lint.missing, suggestion: lint.suggestion });
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Ask the running FluidCAD server to render `code` for `filePath`. Used by
|
|
137
|
+
* `write_file` / `edit_range` to make the agent's edit synchronous: the
|
|
138
|
+
* disk write returns once the render settles, so the caller doesn't need a
|
|
139
|
+
* separate round-trip to observe completion.
|
|
140
|
+
*
|
|
141
|
+
* Non-fatal: any transport error is folded into the outcome as
|
|
142
|
+
* `render-failed` so the agent still sees the write succeeded.
|
|
143
|
+
*/
|
|
144
|
+
async function triggerRender(entry, filePath, code) {
|
|
145
|
+
const client = new FluidCadClient(entry);
|
|
146
|
+
try {
|
|
147
|
+
const outcome = await client.postJson('/api/render', { filePath, code });
|
|
148
|
+
return outcome;
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (e instanceof HttpError && e.statusCode === 404) {
|
|
152
|
+
// Older server without /api/render — silently degrade. The agent still
|
|
153
|
+
// gets `written: true`; the file watcher (under `fluidcad serve`) or
|
|
154
|
+
// the next editor save will eventually trigger the render.
|
|
155
|
+
return { state: 'render-failed', error: 'Server has no /api/render endpoint (upgrade fluidcad).' };
|
|
156
|
+
}
|
|
157
|
+
return { state: 'render-failed', error: e?.message ?? String(e) };
|
|
158
|
+
}
|
|
159
|
+
finally {
|
|
160
|
+
await client.close().catch(() => { });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fetch the editor's dirty-buffer set. Failure to reach the server is
|
|
165
|
+
* non-fatal — the tool will treat the set as empty so the agent can still
|
|
166
|
+
* write when the editor extension is not connected. The MCP description
|
|
167
|
+
* surfaces this caveat.
|
|
168
|
+
*/
|
|
169
|
+
async function fetchDirtyFiles(entry) {
|
|
170
|
+
const client = new FluidCadClient(entry);
|
|
171
|
+
try {
|
|
172
|
+
const dirty = await client.getJson('/api/editor/dirty-files');
|
|
173
|
+
return Array.isArray(dirty) ? dirty : [];
|
|
174
|
+
}
|
|
175
|
+
catch (e) {
|
|
176
|
+
if (e instanceof HttpError && e.statusCode === 404) {
|
|
177
|
+
// Older server with no dirty-files endpoint — treat as none dirty.
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
throw e;
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
await client.close().catch(() => { });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function pathsEqual(a, b) {
|
|
187
|
+
// Best-effort case-insensitive compare on Windows. On Linux/macOS file
|
|
188
|
+
// systems we still mostly want exact match, but realpath canonicalizes
|
|
189
|
+
// case on macOS HFS+, so direct `===` is usually enough.
|
|
190
|
+
if (process.platform === 'win32') {
|
|
191
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
192
|
+
}
|
|
193
|
+
return a === b;
|
|
194
|
+
}
|
|
195
|
+
async function assertNotDirty(entry, absPath, force) {
|
|
196
|
+
if (force === true) {
|
|
197
|
+
return ok(undefined);
|
|
198
|
+
}
|
|
199
|
+
let dirty;
|
|
200
|
+
try {
|
|
201
|
+
dirty = await fetchDirtyFiles(entry);
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
return err('http-error', `Could not check dirty buffers: ${e?.message ?? String(e)}`);
|
|
205
|
+
}
|
|
206
|
+
const conflict = dirty.find((d) => pathsEqual(d.path, absPath));
|
|
207
|
+
if (conflict) {
|
|
208
|
+
return err('dirty-buffer', `Refusing to write "${absPath}" — the editor has unsaved changes. Save in the editor, or pass \`force: true\` to overwrite.`, { dirtyFiles: dirty.map((d) => d.path) });
|
|
209
|
+
}
|
|
210
|
+
return ok(undefined);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Atomic file write: write to a sibling tmp file, fsync it, then rename
|
|
214
|
+
* over the destination. Mirrors `server/src/instance-file.ts` so a crash
|
|
215
|
+
* mid-write never leaves a half-written `.fluid.js` on disk.
|
|
216
|
+
*/
|
|
217
|
+
async function atomicWrite(absPath, content) {
|
|
218
|
+
const dir = path.dirname(absPath);
|
|
219
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
220
|
+
const tmp = path.join(dir, `.${path.basename(absPath)}.${process.pid}.tmp`);
|
|
221
|
+
const fh = await fsp.open(tmp, 'w', 0o644);
|
|
222
|
+
try {
|
|
223
|
+
await fh.writeFile(content, { encoding: 'utf8' });
|
|
224
|
+
await fh.sync();
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
await fh.close();
|
|
228
|
+
}
|
|
229
|
+
await fsp.rename(tmp, absPath);
|
|
230
|
+
}
|
|
231
|
+
export async function readFile(input) {
|
|
232
|
+
const entry = resolveEntry(input);
|
|
233
|
+
if (entry.ok === false) {
|
|
234
|
+
return entry;
|
|
235
|
+
}
|
|
236
|
+
const resolved = resolveWithinWorkspace(entry.data.workspacePath, input?.path, { mustExist: true });
|
|
237
|
+
if (resolved.ok === false) {
|
|
238
|
+
return resolved;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
const content = await fsp.readFile(resolved.data.absPath, 'utf8');
|
|
242
|
+
return ok({ path: resolved.data.absPath, content });
|
|
243
|
+
}
|
|
244
|
+
catch (e) {
|
|
245
|
+
return err('internal', e?.message ?? String(e));
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
export async function writeFile(input) {
|
|
249
|
+
if (typeof input?.content !== 'string') {
|
|
250
|
+
return err('invalid-input', '`content` is required and must be a string.');
|
|
251
|
+
}
|
|
252
|
+
const entry = resolveEntry(input);
|
|
253
|
+
if (entry.ok === false) {
|
|
254
|
+
return entry;
|
|
255
|
+
}
|
|
256
|
+
const resolved = resolveWithinWorkspace(entry.data.workspacePath, input?.path, { mustExist: false });
|
|
257
|
+
if (resolved.ok === false) {
|
|
258
|
+
return resolved;
|
|
259
|
+
}
|
|
260
|
+
const guard = await assertNotDirty(entry.data, resolved.data.absPath, input.force);
|
|
261
|
+
if (guard.ok === false) {
|
|
262
|
+
return guard;
|
|
263
|
+
}
|
|
264
|
+
const importGuard = await assertImportsPresent(entry.data, resolved.data.absPath, input.content, input.force);
|
|
265
|
+
if (importGuard.ok === false) {
|
|
266
|
+
return importGuard;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await atomicWrite(resolved.data.absPath, input.content);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
return err('internal', e?.message ?? String(e));
|
|
273
|
+
}
|
|
274
|
+
const render = await triggerRender(entry.data, resolved.data.absPath, input.content);
|
|
275
|
+
return ok({
|
|
276
|
+
path: resolved.data.absPath,
|
|
277
|
+
bytesWritten: Buffer.byteLength(input.content, 'utf8'),
|
|
278
|
+
render,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
function isValidPosition(value) {
|
|
282
|
+
if (typeof value !== 'object' || value === null) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
const v = value;
|
|
286
|
+
return (typeof v.line === 'number' &&
|
|
287
|
+
Number.isInteger(v.line) &&
|
|
288
|
+
v.line >= 0 &&
|
|
289
|
+
typeof v.column === 'number' &&
|
|
290
|
+
Number.isInteger(v.column) &&
|
|
291
|
+
v.column >= 0);
|
|
292
|
+
}
|
|
293
|
+
function comparePositions(a, b) {
|
|
294
|
+
if (a.line !== b.line) {
|
|
295
|
+
return a.line - b.line;
|
|
296
|
+
}
|
|
297
|
+
return a.column - b.column;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Convert a `{ line, column }` position to a UTF-16 character offset into
|
|
301
|
+
* `text`. Columns past end-of-line clamp to the line's length; lines past
|
|
302
|
+
* end-of-file clamp to the file's length. Both behaviours match LSP's
|
|
303
|
+
* forgiving range semantics and match VSCode's `TextDocument.offsetAt`.
|
|
304
|
+
*/
|
|
305
|
+
function offsetAt(text, pos) {
|
|
306
|
+
let lineStart = 0;
|
|
307
|
+
let line = 0;
|
|
308
|
+
while (line < pos.line) {
|
|
309
|
+
const nl = text.indexOf('\n', lineStart);
|
|
310
|
+
if (nl === -1) {
|
|
311
|
+
return text.length;
|
|
312
|
+
}
|
|
313
|
+
lineStart = nl + 1;
|
|
314
|
+
line++;
|
|
315
|
+
}
|
|
316
|
+
const lineEnd = (() => {
|
|
317
|
+
const nl = text.indexOf('\n', lineStart);
|
|
318
|
+
return nl === -1 ? text.length : nl;
|
|
319
|
+
})();
|
|
320
|
+
return Math.min(lineStart + pos.column, lineEnd);
|
|
321
|
+
}
|
|
322
|
+
export async function editRange(input) {
|
|
323
|
+
if (typeof input?.newText !== 'string') {
|
|
324
|
+
return err('invalid-input', '`newText` is required and must be a string.');
|
|
325
|
+
}
|
|
326
|
+
if (!isValidPosition(input?.start)) {
|
|
327
|
+
return err('invalid-input', '`start` must be `{ line: number >= 0, column: number >= 0 }`.');
|
|
328
|
+
}
|
|
329
|
+
if (!isValidPosition(input?.end)) {
|
|
330
|
+
return err('invalid-input', '`end` must be `{ line: number >= 0, column: number >= 0 }`.');
|
|
331
|
+
}
|
|
332
|
+
if (comparePositions(input.start, input.end) > 0) {
|
|
333
|
+
return err('invalid-input', '`start` must be at or before `end`.');
|
|
334
|
+
}
|
|
335
|
+
const entry = resolveEntry(input);
|
|
336
|
+
if (entry.ok === false) {
|
|
337
|
+
return entry;
|
|
338
|
+
}
|
|
339
|
+
const resolved = resolveWithinWorkspace(entry.data.workspacePath, input?.path, { mustExist: true });
|
|
340
|
+
if (resolved.ok === false) {
|
|
341
|
+
return resolved;
|
|
342
|
+
}
|
|
343
|
+
const guard = await assertNotDirty(entry.data, resolved.data.absPath, input.force);
|
|
344
|
+
if (guard.ok === false) {
|
|
345
|
+
return guard;
|
|
346
|
+
}
|
|
347
|
+
let original;
|
|
348
|
+
try {
|
|
349
|
+
original = await fsp.readFile(resolved.data.absPath, 'utf8');
|
|
350
|
+
}
|
|
351
|
+
catch (e) {
|
|
352
|
+
return err('internal', e?.message ?? String(e));
|
|
353
|
+
}
|
|
354
|
+
const startOffset = offsetAt(original, input.start);
|
|
355
|
+
const endOffset = offsetAt(original, input.end);
|
|
356
|
+
const next = original.slice(0, startOffset) + input.newText + original.slice(endOffset);
|
|
357
|
+
const importGuard = await assertImportsPresent(entry.data, resolved.data.absPath, next, input.force);
|
|
358
|
+
if (importGuard.ok === false) {
|
|
359
|
+
return importGuard;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
await atomicWrite(resolved.data.absPath, next);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
return err('internal', e?.message ?? String(e));
|
|
366
|
+
}
|
|
367
|
+
const render = await triggerRender(entry.data, resolved.data.absPath, next);
|
|
368
|
+
return ok({
|
|
369
|
+
path: resolved.data.absPath,
|
|
370
|
+
bytesWritten: Buffer.byteLength(next, 'utf8'),
|
|
371
|
+
replacedRange: { start: input.start, end: input.end },
|
|
372
|
+
render,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// list_fluid_files
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.fluidcad', 'dist', 'build']);
|
|
379
|
+
const FLUID_SUFFIX = '.fluid.js';
|
|
380
|
+
const WALK_FILE_LIMIT = 5000;
|
|
381
|
+
export async function listFluidFiles(input) {
|
|
382
|
+
const entry = resolveEntry(input);
|
|
383
|
+
if (entry.ok === false) {
|
|
384
|
+
return entry;
|
|
385
|
+
}
|
|
386
|
+
const root = (() => {
|
|
387
|
+
try {
|
|
388
|
+
return fs.realpathSync(entry.data.workspacePath);
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
return path.resolve(entry.data.workspacePath);
|
|
392
|
+
}
|
|
393
|
+
})();
|
|
394
|
+
const files = [];
|
|
395
|
+
try {
|
|
396
|
+
await walk(root, root, files);
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
return err('internal', e?.message ?? String(e));
|
|
400
|
+
}
|
|
401
|
+
files.sort();
|
|
402
|
+
return ok({ files });
|
|
403
|
+
}
|
|
404
|
+
async function walk(root, dir, out) {
|
|
405
|
+
if (out.length >= WALK_FILE_LIMIT) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
let entries;
|
|
409
|
+
try {
|
|
410
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
for (const e of entries) {
|
|
416
|
+
if (out.length >= WALK_FILE_LIMIT) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (e.name.startsWith('.') && e.isDirectory()) {
|
|
420
|
+
// Hidden dirs (`.git`, `.fluidcad`, …) — always skip.
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
if (e.isDirectory() && SKIP_DIRS.has(e.name)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const full = path.join(dir, e.name);
|
|
427
|
+
if (e.isDirectory()) {
|
|
428
|
+
await walk(root, full, out);
|
|
429
|
+
}
|
|
430
|
+
else if (e.isFile() && e.name.endsWith(FLUID_SUFFIX)) {
|
|
431
|
+
out.push(path.relative(root, full));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ToolResult } from '../types.ts';
|
|
2
|
+
export type WorkspaceInfo = {
|
|
3
|
+
workspacePath: string;
|
|
4
|
+
port: number;
|
|
5
|
+
pid: number;
|
|
6
|
+
version: string;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
reachable: boolean;
|
|
9
|
+
};
|
|
10
|
+
export type ListWorkspacesOutput = {
|
|
11
|
+
workspaces: WorkspaceInfo[];
|
|
12
|
+
};
|
|
13
|
+
export declare function listWorkspaces(): Promise<ToolResult<ListWorkspacesOutput>>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Workspace enumeration tool — the agent's entry point for discovery.
|
|
2
|
+
//
|
|
3
|
+
// Returns every running FluidCAD workspace on this machine, with a quick
|
|
4
|
+
// health probe so dead-but-not-yet-pruned entries surface as `reachable: false`
|
|
5
|
+
// instead of being silently dropped.
|
|
6
|
+
import { listLiveInstances } from "../discovery.js";
|
|
7
|
+
import { FluidCadClient } from "../client.js";
|
|
8
|
+
import { ok } from "../types.js";
|
|
9
|
+
export async function listWorkspaces() {
|
|
10
|
+
const entries = listLiveInstances();
|
|
11
|
+
const probes = await Promise.all(entries.map(async (entry) => {
|
|
12
|
+
const client = new FluidCadClient(entry);
|
|
13
|
+
try {
|
|
14
|
+
const health = await client.health();
|
|
15
|
+
return { entry, reachable: health !== null };
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
// We don't keep clients around past the probe — each tool invocation
|
|
19
|
+
// re-creates them when needed. Pools are cheap; lingering connections
|
|
20
|
+
// are not.
|
|
21
|
+
await client.close().catch(() => { });
|
|
22
|
+
}
|
|
23
|
+
}));
|
|
24
|
+
const workspaces = probes.map(({ entry, reachable }) => ({
|
|
25
|
+
workspacePath: entry.workspacePath,
|
|
26
|
+
port: entry.port,
|
|
27
|
+
pid: entry.pid,
|
|
28
|
+
version: entry.version,
|
|
29
|
+
startedAt: entry.startedAt,
|
|
30
|
+
reachable,
|
|
31
|
+
}));
|
|
32
|
+
return ok({ workspaces });
|
|
33
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type { InstanceFile } from '../../server/dist/instance-file.js';
|
|
2
|
+
export type { RegistryEntry } from '../../server/dist/global-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Discriminated result type used by every tool handler so the MCP layer can
|
|
5
|
+
* render success/failure consistently and the agent can branch on `code`.
|
|
6
|
+
*/
|
|
7
|
+
export type ToolResult<T> = {
|
|
8
|
+
ok: true;
|
|
9
|
+
data: T;
|
|
10
|
+
} | {
|
|
11
|
+
ok: false;
|
|
12
|
+
code: ToolErrorCode;
|
|
13
|
+
message: string;
|
|
14
|
+
details?: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type ToolErrorCode = 'no-server' | 'no-workspace' | 'workspace-not-found' | 'http-error' | 'ws-error' | 'invalid-input' | 'timeout' | 'compile-error' | 'dirty-buffer' | 'missing-imports' | 'internal';
|
|
17
|
+
export declare function ok<T>(data: T): ToolResult<T>;
|
|
18
|
+
export declare function err(code: ToolErrorCode, message: string, details?: unknown): ToolResult<never>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Types shared across the MCP server.
|
|
2
|
+
//
|
|
3
|
+
// We deliberately re-export the discovery types from the server package
|
|
4
|
+
// rather than redefining them — the contract is owned by `server/src/` and
|
|
5
|
+
// imported via the project reference to the built `.d.ts` output.
|
|
6
|
+
export function ok(data) {
|
|
7
|
+
return { ok: true, data };
|
|
8
|
+
}
|
|
9
|
+
export function err(code, message, details) {
|
|
10
|
+
return { ok: false, code, message, details };
|
|
11
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fluidcad",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.34",
|
|
4
4
|
"description": "Parametric CAD modeling library using javascript",
|
|
5
5
|
"author": "Marwan Aouida <contact@marwan.dev>",
|
|
6
6
|
"homepage": "https://fluidcad.io",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"lib/dist/",
|
|
30
30
|
"server/dist/",
|
|
31
31
|
"ui/dist/",
|
|
32
|
-
"
|
|
32
|
+
"mcp/dist/",
|
|
33
|
+
"bin/",
|
|
34
|
+
"llm-docs/"
|
|
33
35
|
],
|
|
34
36
|
"exports": {
|
|
35
37
|
".": "./lib/dist/index.js",
|
|
@@ -40,11 +42,15 @@
|
|
|
40
42
|
"./math": "./lib/dist/math/index.js"
|
|
41
43
|
},
|
|
42
44
|
"scripts": {
|
|
43
|
-
"clean": "rm -rf lib/dist server/dist ui/dist lib/tsconfig.tsbuildinfo server/tsconfig.tsbuildinfo",
|
|
45
|
+
"clean": "rm -rf lib/dist server/dist ui/dist mcp/dist lib/tsconfig.tsbuildinfo server/tsconfig.tsbuildinfo mcp/tsconfig.tsbuildinfo",
|
|
44
46
|
"build:lib": "tsc -p lib/tsconfig.json",
|
|
45
47
|
"build:server": "tsc -p server/tsconfig.json",
|
|
46
48
|
"build:ui": "vite build --config ui/vite.config.ts",
|
|
47
|
-
"build": "
|
|
49
|
+
"build:mcp": "tsc -p mcp/tsconfig.json",
|
|
50
|
+
"build:llm-type-docs": "tsx scripts/build-llm-type-docs.ts",
|
|
51
|
+
"build:llm-docs": "npm run build:llm-type-docs && tsx scripts/build-llm-docs.ts",
|
|
52
|
+
"check:llm-docs": "npm run build:llm-docs && tsx scripts/check-llm-docs-coverage.ts && vitest run lib/tests/llm-docs-examples.test.ts",
|
|
53
|
+
"build": "npm run clean && npm run build:lib && npm run build:server && npm run build:ui && npm run build:mcp && npm run build:llm-docs",
|
|
48
54
|
"prepublishOnly": "npm run build",
|
|
49
55
|
"dev:lib": "tsc -p lib/tsconfig.json --watch",
|
|
50
56
|
"dev:server": "node --experimental-transform-types --no-warnings server/src/index.ts",
|
|
@@ -55,32 +61,40 @@
|
|
|
55
61
|
"release": "npx bumpp patch package.json extension/vscode/package.json"
|
|
56
62
|
},
|
|
57
63
|
"dependencies": {
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
65
|
"chokidar": "^5.0.0",
|
|
59
66
|
"color-name": "^2.1.0",
|
|
67
|
+
"commander": "^14.0.3",
|
|
60
68
|
"express": "^5.2.1",
|
|
61
69
|
"occjs-wrapper": "npm:occjs-fluidcad@8.0.0",
|
|
70
|
+
"open": "^11.0.0",
|
|
62
71
|
"stacktrace-parser": "^0.1.11",
|
|
63
72
|
"tree-sitter-wasms": "^0.1.13",
|
|
64
73
|
"tsx": "^4.21.0",
|
|
74
|
+
"undici": "^8.3.0",
|
|
65
75
|
"vite": "^8.0.8",
|
|
66
76
|
"web-tree-sitter": "^0.24.7",
|
|
67
|
-
"ws": "^8.18.0"
|
|
77
|
+
"ws": "^8.18.0",
|
|
78
|
+
"zod": "^4.4.3"
|
|
68
79
|
},
|
|
69
80
|
"devDependencies": {
|
|
70
81
|
"@tabler/icons": "^3.40.0",
|
|
71
82
|
"@tailwindcss/vite": "^4.2.2",
|
|
72
83
|
"@types/express": "^5.0.6",
|
|
84
|
+
"@types/js-yaml": "^4.0.9",
|
|
73
85
|
"@types/node": "^22.14.1",
|
|
74
86
|
"@types/three": "^0.180.0",
|
|
75
87
|
"@types/ws": "^8.18.0",
|
|
76
88
|
"camera-controls": "^3.1.2",
|
|
77
89
|
"daisyui": "^5.5.19",
|
|
78
90
|
"eslint": "^9.24.0",
|
|
91
|
+
"js-yaml": "^4.1.1",
|
|
79
92
|
"lodash": "^4.17.21",
|
|
80
93
|
"prettier": "^3.5.3",
|
|
81
94
|
"tailwindcss": "^4.2.2",
|
|
82
95
|
"three": "^0.180.0",
|
|
83
96
|
"three-viewport-gizmo": "^2.2.0",
|
|
97
|
+
"ts-morph": "^28.0.0",
|
|
84
98
|
"typescript": "^5.9.2",
|
|
85
99
|
"vitest": "^4.0.17"
|
|
86
100
|
}
|
|
@@ -1,3 +1,38 @@
|
|
|
1
|
+
type TSNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
text: string;
|
|
4
|
+
startPosition: {
|
|
5
|
+
row: number;
|
|
6
|
+
column: number;
|
|
7
|
+
};
|
|
8
|
+
endPosition: {
|
|
9
|
+
row: number;
|
|
10
|
+
column: number;
|
|
11
|
+
};
|
|
12
|
+
startIndex: number;
|
|
13
|
+
endIndex: number;
|
|
14
|
+
parent: TSNode | null;
|
|
15
|
+
namedChildren: TSNode[];
|
|
16
|
+
namedChild(i: number): TSNode | null;
|
|
17
|
+
childForFieldName(name: string): TSNode | null;
|
|
18
|
+
descendantForPosition(pos: {
|
|
19
|
+
row: number;
|
|
20
|
+
column: number;
|
|
21
|
+
}): TSNode | null;
|
|
22
|
+
};
|
|
23
|
+
type TSTree = {
|
|
24
|
+
rootNode: TSNode;
|
|
25
|
+
};
|
|
26
|
+
type TSParser = {
|
|
27
|
+
setLanguage(lang: any): void;
|
|
28
|
+
parse(code: string): TSTree;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Public alias for `getParser()` so other modules in this package (e.g.
|
|
32
|
+
* `lint-fluid-js.ts`) can reuse the same wasm-backed parser instance instead
|
|
33
|
+
* of loading the JavaScript grammar twice.
|
|
34
|
+
*/
|
|
35
|
+
export declare function getJavaScriptParser(): Promise<TSParser>;
|
|
1
36
|
export type BreakpointEditResult = {
|
|
2
37
|
newCode: string;
|
|
3
38
|
breakpointLine: number | null;
|
|
@@ -19,3 +54,4 @@ export declare function addPick(code: string, sourceLine: number): Promise<CodeE
|
|
|
19
54
|
export declare function removePick(code: string, sourceLine: number): Promise<CodeEditResult>;
|
|
20
55
|
export declare function removePoint(code: string, sourceLine: number, point: [number, number]): Promise<CodeEditResult>;
|
|
21
56
|
export declare function setPickPoints(code: string, sourceLine: number, points: [number, number][]): Promise<CodeEditResult>;
|
|
57
|
+
export {};
|
|
@@ -5,6 +5,14 @@ async function loadTreeSitter() {
|
|
|
5
5
|
return mod.default;
|
|
6
6
|
}
|
|
7
7
|
let parser = null;
|
|
8
|
+
/**
|
|
9
|
+
* Public alias for `getParser()` so other modules in this package (e.g.
|
|
10
|
+
* `lint-fluid-js.ts`) can reuse the same wasm-backed parser instance instead
|
|
11
|
+
* of loading the JavaScript grammar twice.
|
|
12
|
+
*/
|
|
13
|
+
export async function getJavaScriptParser() {
|
|
14
|
+
return getParser();
|
|
15
|
+
}
|
|
8
16
|
async function getParser() {
|
|
9
17
|
if (parser) {
|
|
10
18
|
return parser;
|