forma-dsl-mcp 0.3.7 → 0.3.9

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 (41) hide show
  1. package/README.md +18 -18
  2. package/package.json +3 -3
  3. package/src/bin.js +1 -1
  4. package/src/catalogue.js +15 -15
  5. package/src/document.js +8 -38
  6. package/src/examples.js +1 -1
  7. package/src/guide.js +24 -24
  8. package/src/reference/README.md +15 -5
  9. package/src/reference/api/describe-parameters.md +4 -4
  10. package/src/reference/api/evaluation-context.md +13 -13
  11. package/src/reference/api/export.md +7 -7
  12. package/src/reference/api/geometry-node.md +9 -9
  13. package/src/reference/api/kernel.md +8 -8
  14. package/src/reference/api/low-level.md +9 -9
  15. package/src/reference/api/render.md +8 -8
  16. package/src/reference/api/values.md +12 -12
  17. package/src/reference/cli.md +146 -0
  18. package/src/reference/errors.md +14 -14
  19. package/src/reference/language/control-flow.md +3 -3
  20. package/src/reference/language/expressions.md +10 -10
  21. package/src/reference/language/overview.md +11 -11
  22. package/src/reference/language/scope.md +13 -13
  23. package/src/reference/language/syntax.md +4 -4
  24. package/src/reference/reference/align.md +9 -9
  25. package/src/reference/reference/booleans.md +5 -5
  26. package/src/reference/reference/component.md +12 -12
  27. package/src/reference/reference/conversions.md +8 -8
  28. package/src/reference/reference/functions.md +8 -8
  29. package/src/reference/reference/local.md +4 -4
  30. package/src/reference/reference/model.md +10 -10
  31. package/src/reference/reference/param.md +8 -8
  32. package/src/reference/reference/part.md +12 -12
  33. package/src/reference/reference/refinement.md +10 -10
  34. package/src/reference/reference/shapes-2d.md +15 -15
  35. package/src/reference/reference/shapes-3d.md +13 -13
  36. package/src/reference/reference/transforms.md +10 -10
  37. package/src/reference.js +4 -3
  38. package/src/roots.js +2 -2
  39. package/src/server.js +2 -2
  40. package/src/tools.js +15 -15
  41. package/src/workspace.js +7 -7
@@ -11,7 +11,7 @@ Five leaf blocks producing solids. They take attributes and never contain other
11
11
  | [`torus`](#torus) | `radius`, `tube_radius` | always |
12
12
 
13
13
  Anything these cannot express is usually a 2D profile plus
14
- [`extrude`](conversions.md#extrude) or [`revolve`](conversions.md#revolve) — that is where
14
+ [`extrude`](conversions.md#extrude) or [`revolve`](conversions.md#revolve) - that is where
15
15
  the real modelling happens.
16
16
 
17
17
  ---
@@ -35,7 +35,7 @@ box { size = [40, 20, 10] center = true }
35
35
  #### Caveats
36
36
 
37
37
  - A single number is accepted as shorthand for all three, so `size = 10` is a 10 mm cube.
38
- - There is no rounded box. Round a 2D profile and extrude it —
38
+ - There is no rounded box. Round a 2D profile and extrude it -
39
39
  see [Usage](#rounded-boxes).
40
40
 
41
41
  ---
@@ -52,7 +52,7 @@ sphere { diameter = 24 segments = 64 }
52
52
  | Attribute | Type | Default | Description |
53
53
  | --- | --- | --- | --- |
54
54
  | `radius` | number | `1` | |
55
- | `diameter` | number | — | An alternative spelling of the same thing |
55
+ | `diameter` | number | - | An alternative spelling of the same thing |
56
56
  | `segments` | integer | `0` | Segments around the equator; `0` lets the kernel choose |
57
57
 
58
58
  #### Caveats
@@ -77,17 +77,17 @@ cylinder { bottom_radius = 12 top_radius = 8 height = 15 }
77
77
  | Attribute | Type | Default | Description |
78
78
  | --- | --- | --- | --- |
79
79
  | `height` | number | **required** | Along Z |
80
- | `radius` | number | — | Both ends alike |
81
- | `diameter` | number | — | An alternative spelling of `radius` |
82
- | `bottom_radius` | number | — | The end at Z = 0 |
83
- | `top_radius` | number | — | The end at Z = `height` |
80
+ | `radius` | number | - | Both ends alike |
81
+ | `diameter` | number | - | An alternative spelling of `radius` |
82
+ | `bottom_radius` | number | - | The end at Z = 0 |
83
+ | `top_radius` | number | - | The end at Z = `height` |
84
84
  | `segments` | integer | `0` | Segments around; `0` lets the kernel choose |
85
85
  | `center` | bool | `false` | `true` centres it on Z = 0; otherwise it stands on the XY plane |
86
86
 
87
87
  #### Caveats
88
88
 
89
89
  - Give **either** `radius`/`diameter`, **or both** `bottom_radius` and `top_radius`. Anything
90
- else is `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` —
90
+ else is `cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius"` -
91
91
  one radius on its own is ambiguous, and there is no default.
92
92
  - Unlike every other shape, `radius` has no usable default here: the check runs before the
93
93
  fallback would apply.
@@ -110,7 +110,7 @@ cone { radius = 10 top_radius = 4 height = 20 }
110
110
  | --- | --- | --- | --- |
111
111
  | `height` | number | **required** | Along Z |
112
112
  | `radius` | number | `1` | The base, at Z = 0 |
113
- | `diameter` | number | — | An alternative spelling of `radius` |
113
+ | `diameter` | number | - | An alternative spelling of `radius` |
114
114
  | `top_radius` | number | `0` | `0` gives a point |
115
115
  | `segments` | integer | `0` | Segments around |
116
116
  | `center` | bool | `false` | As for [`cylinder`](#cylinder) |
@@ -118,7 +118,7 @@ cone { radius = 10 top_radius = 4 height = 20 }
118
118
  #### Caveats
119
119
 
120
120
  - `cone` and `cylinder` build the same kind of solid; they differ only in which attributes
121
- are required and what `top_radius` defaults to. Use whichever reads better — `cone` when
121
+ are required and what `top_radius` defaults to. Use whichever reads better - `cone` when
122
122
  the top is a point, `cylinder` when both ends matter.
123
123
 
124
124
  ---
@@ -158,7 +158,7 @@ torus { radius = 20 tube_radius = 4 angle = 180 }
158
158
 
159
159
  ### Rounded boxes
160
160
 
161
- There is no rounded-box primitive. Round the 2D profile and extrude it — which also gives
161
+ There is no rounded-box primitive. Round the 2D profile and extrude it - which also gives
162
162
  control over whether the vertical edges or the horizontal ones are rounded:
163
163
 
164
164
  ```hcl
@@ -270,8 +270,8 @@ box { size = [40, 20, 10] } // ✓
270
270
  ### My cylinder is not where I expect
271
271
 
272
272
  By default a cylinder stands **on** the XY plane, with its base at Z = 0. `center = true`
273
- puts its middle there instead. The same is true of `box`, whose minimum corner — not its
274
- centre — sits at the origin by default.
273
+ puts its middle there instead. The same is true of `box`, whose minimum corner - not its
274
+ centre - sits at the origin by default.
275
275
 
276
276
  ### Two solids that touch exactly render with artefacts
277
277
 
@@ -36,7 +36,7 @@ translate { offset = [10, 0] rect { size = [4, 4] } } // 2D
36
36
 
37
37
  #### Caveats
38
38
 
39
- - `offset` must have as many components as the child has dimensions — 2 for a cross-section,
39
+ - `offset` must have as many components as the child has dimensions - 2 for a cross-section,
40
40
  3 for a solid. A mismatch is `translate: "offset" must be a number or an array of 3
41
41
  numbers`.
42
42
  - A single number broadcasts to every axis, which is rarely what you want here but is
@@ -54,14 +54,14 @@ rotate { angles = [0, 90, 45] box { size = [20, 4, 4] } } // 3D, all three
54
54
  rotate { angle = 30 rect { size = [20, 4] } } // 2D
55
55
  ```
56
56
 
57
- #### Attributes — 3D children
57
+ #### Attributes - 3D children
58
58
 
59
59
  | Attribute | Type | Default | Description |
60
60
  | --- | --- | --- | --- |
61
- | `angles` | 3-vector | — | Degrees about X, Y and Z at once |
61
+ | `angles` | 3-vector | - | Degrees about X, Y and Z at once |
62
62
  | `x`, `y`, `z` | number | `0` | Degrees about that axis, named one at a time |
63
63
 
64
- #### Attributes — 2D children
64
+ #### Attributes - 2D children
65
65
 
66
66
  | Attribute | Type | Default | Description |
67
67
  | --- | --- | --- | --- |
@@ -70,7 +70,7 @@ rotate { angle = 30 rect { size = [20, 4] } } // 2D
70
70
  #### Caveats
71
71
 
72
72
  - **Rotation is about the origin, not about the geometry.** To spin something in place, put
73
- it at the origin, rotate, then translate — or build it centred in the first place.
73
+ it at the origin, rotate, then translate - or build it centred in the first place.
74
74
  - In 3D the axes are applied **X, then Y, then Z**, matching OpenSCAD, so a model ported from
75
75
  there lands in the same orientation. Composing rotations in a different order requires
76
76
  separate `rotate` blocks.
@@ -100,7 +100,7 @@ scale { factor = [1, 1, 0.5] sphere { radius = 5 } } // flattened
100
100
  - A negative factor mirrors as well as scales, and flips the winding. Prefer
101
101
  [`mirror`](#mirror) when reflection is what you mean.
102
102
  - A factor of `0` collapses the geometry to nothing.
103
- - Non-uniform scaling of a curved surface changes its radii — a scaled sphere is an
103
+ - Non-uniform scaling of a curved surface changes its radii - a scaled sphere is an
104
104
  ellipsoid, which is the cheapest way to get one.
105
105
 
106
106
  ---
@@ -167,7 +167,7 @@ translate { // …then moved 50 along X
167
167
  }
168
168
  ```
169
169
 
170
- Swapping the two gives a different result — rotating a translated box sweeps it around the
170
+ Swapping the two gives a different result - rotating a translated box sweeps it around the
171
171
  origin on an arc of radius 50.
172
172
 
173
173
  ### Radial patterns
@@ -183,7 +183,7 @@ for i in range(0, 8) {
183
183
  }
184
184
  ```
185
185
 
186
- Because identity is content identity, this costs **one** cylinder and eight transforms — the
186
+ Because identity is content identity, this costs **one** cylinder and eight transforms - the
187
187
  cylinder node is the same node every time round the loop.
188
188
 
189
189
  ### Mirrored pairs
@@ -212,7 +212,7 @@ scale { factor = [1, 0.6, 1] sphere { radius = 20 segments = 64 } }
212
212
  ### `translate: "offset" must be a number or an array of 3 numbers`
213
213
 
214
214
  The offset has the wrong number of components for its child. 2D children want two, 3D
215
- children want three — and a 2D shape stays 2D until it is extruded:
215
+ children want three - and a 2D shape stays 2D until it is extruded:
216
216
 
217
217
  ```hcl
218
218
  translate { offset = [1, 2, 3] rect { size = [4, 4] } } // ✗ rect is 2D
@@ -228,7 +228,7 @@ rotate { angle = 45 box { size = [1, 1, 1] } } // ✗
228
228
  rotate { z = 45 box { size = [1, 1, 1] } } // ✓
229
229
  ```
230
230
 
231
- The reverse — `z` on a 2D child — fails the same way.
231
+ The reverse - `z` on a 2D child - fails the same way.
232
232
 
233
233
  ### `"translate" needs at least one shape inside it`
234
234
 
package/src/reference.js CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * The manual is the language's own documentation, copied into the package by
5
5
  * `scripts/sync-reference.mjs` rather than paraphrased, so an answer from here is the same
6
- * answer a person reading the published docs would get. Search is deliberately simple —
7
- * substring matching over headings and body text, ranked — because the corpus is 28 pages
6
+ * answer a person reading the published docs would get. Search is deliberately simple -
7
+ * substring matching over headings and body text, ranked - because the corpus is 29 pages
8
8
  * and anything cleverer would be a dependency and a source of surprise.
9
9
  */
10
10
  import { readFile, readdir } from 'node:fs/promises';
@@ -47,6 +47,7 @@ export const PAGES = [
47
47
  { path: 'reference/part.md', title: 'part', about: 'Coloured pieces of the rendered scene' },
48
48
  { path: 'reference/functions.md', title: 'Functions', about: 'Every builtin function and constant' },
49
49
  { path: 'errors.md', title: 'Errors', about: 'Every error message, what raises it, and what to change' },
50
+ { path: 'cli.md', title: 'Command line', about: 'The forma command: render to STL, list params and models, exit codes' },
50
51
  { path: 'api/render.md', title: 'render', about: 'JavaScript API: compile a document and evaluate a model' },
51
52
  { path: 'api/describe-parameters.md', title: 'describeParameters', about: 'JavaScript API: read a document’s params without rendering' },
52
53
  { path: 'api/evaluation-context.md', title: 'EvaluationContext', about: 'JavaScript API: the digest cache and WASM ownership' },
@@ -95,7 +96,7 @@ export async function readPage(path) {
95
96
  const page = BY_PATH.get(normalised);
96
97
  if (!page) {
97
98
  throw new Error(
98
- `no reference page "${path}" — the pages are ${PAGES.map((each) => each.path).join(', ')}`,
99
+ `no reference page "${path}" - the pages are ${PAGES.map((each) => each.path).join(', ')}`,
99
100
  );
100
101
  }
101
102
  return { path: page.path, title: page.title, markdown: await readFile(join(ROOT, page.path), 'utf8') };
package/src/roots.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * The server does not get to choose the patch of disk it writes to. The MCP `roots`
5
5
  * capability is the client's declaration of which directories it is handing over, and asking
6
- * for it — rather than reading an environment variable the client cannot see — is the whole
6
+ * for it - rather than reading an environment variable the client cannot see - is the whole
7
7
  * of this module. A client that declares nothing gets the directory it launched the server
8
8
  * in, which is the one location it did choose.
9
9
  */
@@ -17,7 +17,7 @@ import { Workspace } from './workspace.js';
17
17
  * The part of the low-level server this module needs.
18
18
  *
19
19
  * Named structurally rather than as the SDK's `Server`, which is deprecated in favour of
20
- * `McpServer` — but `McpServer` does not carry `listRoots` or `setNotificationHandler`, so the
20
+ * `McpServer` - but `McpServer` does not carry `listRoots` or `setNotificationHandler`, so the
21
21
  * low-level object is genuinely the right one to take. Describing the three members used is
22
22
  * both the honest contract and what lets a test hand this a stub.
23
23
  *
package/src/server.js CHANGED
@@ -43,7 +43,7 @@ export const VERSION = createRequire(import.meta.url)('../package.json').version
43
43
  const INSTRUCTIONS = `This server knows forma, a declarative DSL for 3D solids, builds it with
44
44
  the real geometry kernel, and writes the documents it builds.
45
45
 
46
- Start with \`forma_guide\` — it is the language in one page. Use \`forma_blocks\` for the
46
+ Start with \`forma_guide\` - it is the language in one page. Use \`forma_blocks\` for the
47
47
  attributes of the blocks you are about to write, \`forma_examples\` for a working document of
48
48
  a similar shape, and \`forma_reference\` when you need the manual.
49
49
 
@@ -61,7 +61,7 @@ first of them. Every write reports the directory it landed in.`;
61
61
  * Builds the server, with every tool and resource registered.
62
62
  *
63
63
  * The workspace is optional, and left out is the normal case: the server then takes its
64
- * directories from whatever the connected client declares as its roots, which is the point —
64
+ * directories from whatever the connected client declares as its roots, which is the point -
65
65
  * the client knows where the user's files belong and the server does not.
66
66
  *
67
67
  * @param {Workspace} [workspace] The patch of filesystem the tools may touch. Omitted, the
package/src/tools.js CHANGED
@@ -23,7 +23,7 @@ import { checkDocument, exportStl } from './document.js';
23
23
  * All four hints are stated rather than just `readOnlyHint`, because the omitted ones are not
24
24
  * absent: the specification gives `destructiveHint` a default of **true** and `idempotentHint`
25
25
  * a default of **false**. A tool declaring neither is therefore advertising itself as
26
- * destructive and unrepeatable — which, for a call that hands back a page of the manual, is
26
+ * destructive and unrepeatable - which, for a call that hands back a page of the manual, is
27
27
  * simply untrue, and a cautious client is entitled to act on it.
28
28
  *
29
29
  * `openWorldHint` is false because every answer is bundled with the package. Nothing here
@@ -52,8 +52,8 @@ const say = (text, isError = false) => ({
52
52
  * Runs a handler, turning anything it throws into a result the caller can read.
53
53
  *
54
54
  * A thrown error becomes a protocol-level failure, which most clients surface as "the tool
55
- * broke" rather than as something the model can correct. A refusal it can read — a path
56
- * outside the roots, a part that does not exist — belongs in the result instead.
55
+ * broke" rather than as something the model can correct. A refusal it can read - a path
56
+ * outside the roots, a part that does not exist - belongs in the result instead.
57
57
  *
58
58
  * @param {() => Promise<object>} handler What to run.
59
59
  * @returns {Promise<object>} Its result, or the failure as text.
@@ -115,9 +115,9 @@ function parameterLines(parameters) {
115
115
  return [
116
116
  `\`${parameter.name}\``,
117
117
  parameter.type,
118
- parameter.default === undefined ? '—' : JSON.stringify(parameter.default),
118
+ parameter.default === undefined ? '-' : JSON.stringify(parameter.default),
119
119
  parameter.required ? 'yes' : 'no',
120
- bounded ? `${parameter.min ?? '−∞'} … ${parameter.max ?? '∞'}` : '—',
120
+ bounded ? `${parameter.min ?? '−∞'} … ${parameter.max ?? '∞'}` : '-',
121
121
  ];
122
122
  }),
123
123
  ), ''];
@@ -184,7 +184,7 @@ function sceneLines(scene, stats) {
184
184
  function takesOf(entry) {
185
185
  if (entry.leaf) return 'nothing';
186
186
  if (entry.takes === 'same') return '2D or 3D';
187
- return entry.takes ? `${entry.takes}D` : '—';
187
+ return entry.takes ? `${entry.takes}D` : '-';
188
188
  }
189
189
 
190
190
  /**
@@ -224,7 +224,7 @@ async function documentFrom(workspace, { source, path }) {
224
224
  if (source && path) throw new Error('give either "source" or "path", not both');
225
225
  if (source) return source;
226
226
  if (path) return workspace.read(path);
227
- throw new Error('give either "source" — the document itself — or "path" to one already written');
227
+ throw new Error('give either "source" - the document itself - or "path" to one already written');
228
228
  }
229
229
 
230
230
  /**
@@ -304,7 +304,7 @@ function registerBlocks(server) {
304
304
  ['Block', 'Dim', 'Takes', 'Summary'],
305
305
  all.filter((entry) => entry.group === name).map((entry) => [
306
306
  `\`${entry.name}\``,
307
- entry.dim ? `${entry.dim}D` : '—',
307
+ entry.dim ? `${entry.dim}D` : '-',
308
308
  takesOf(entry),
309
309
  entry.summary,
310
310
  ]),
@@ -319,7 +319,7 @@ function registerBlocks(server) {
319
319
  * Every function an expression may call, and the two named constants.
320
320
  *
321
321
  * There are no user-defined functions in forma, so this answer is complete rather than a
322
- * sample — which is worth saying, because it is what stops a caller inventing one.
322
+ * sample - which is worth saying, because it is what stops a caller inventing one.
323
323
  *
324
324
  * @param {McpServer} server The server.
325
325
  * @returns {void}
@@ -363,7 +363,7 @@ function registerReference(server) {
363
363
  description:
364
364
  'The language\'s full reference manual, bundled with this server. Pass `query` to '
365
365
  + 'search it, or `page` to read one page whole. Reach for this when forma_blocks is not '
366
- + 'enough — the manual has the worked examples, the caveats and the troubleshooting for '
366
+ + 'enough - the manual has the worked examples, the caveats and the troubleshooting for '
367
367
  + 'every error message the language raises. Call with no arguments to list the pages.',
368
368
  inputSchema: {
369
369
  query: z.string().optional().describe('Words to search for, e.g. "revolve profile axis".'),
@@ -383,7 +383,7 @@ function registerReference(server) {
383
383
  `# ${hits.length} page${plural} matching "${query}"`,
384
384
  '',
385
385
  ...hits.map((hit) => [
386
- `## ${hit.title} — \`${hit.path}\``,
386
+ `## ${hit.title} - \`${hit.path}\``,
387
387
  hit.heading ? `_under "${hit.heading}"_` : '',
388
388
  '',
389
389
  '> ' + hit.snippet.replaceAll('\n', '\n> '),
@@ -468,7 +468,7 @@ function registerWrite(server, workspace) {
468
468
  + 'triangle count and genus, or the error with the line, the column and an excerpt with '
469
469
  + 'a caret on it. This is how you find out whether what you wrote is the size you meant, '
470
470
  + 'is watertight, and is not silently empty. A document that does not render is refused '
471
- + 'and nothing is written — pass `force: true` to write it anyway. Refuses to replace an '
471
+ + 'and nothing is written - pass `force: true` to write it anyway. Refuses to replace an '
472
472
  + 'existing file unless `overwrite` is set.',
473
473
  inputSchema: {
474
474
  path: z.string()
@@ -494,7 +494,7 @@ function registerWrite(server, workspace) {
494
494
 
495
495
  const written = await workspace.write(path, source, { overwrite, extension: '.forma' });
496
496
  return say([
497
- `Wrote \`${written.relative}\` under \`${written.root}\` — ${written.bytes} bytes`
497
+ `Wrote \`${written.relative}\` under \`${written.root}\` - ${written.bytes} bytes`
498
498
  + `${written.replaced ? ', replacing what was there' : ''}.`,
499
499
  '',
500
500
  formatReport(report),
@@ -546,7 +546,7 @@ function registerExportStl(server, workspace) {
546
546
  server.registerTool('forma_export_stl', {
547
547
  title: 'Render and write a binary STL',
548
548
  description:
549
- 'Renders a document and writes one part — or every part unioned — as a binary STL. '
549
+ 'Renders a document and writes one part - or every part unioned - as a binary STL. '
550
550
  + 'Lengths are unitless in forma and STL has no unit either; every consumer treats them '
551
551
  + 'as millimetres.',
552
552
  inputSchema: {
@@ -575,7 +575,7 @@ function registerExportStl(server, workspace) {
575
575
  extension: '.stl',
576
576
  });
577
577
  return say(
578
- `Wrote \`${written.relative}\` under \`${written.root}\` — ${written.bytes} bytes, `
578
+ `Wrote \`${written.relative}\` under \`${written.root}\` - ${written.bytes} bytes, `
579
579
  + `${result.triangles} triangles, from model \`${result.model}\`, part \`${result.part}\`.`,
580
580
  );
581
581
  }));
package/src/workspace.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * An MCP server writes files on behalf of a model, so the set of paths it will accept is a
5
5
  * security boundary rather than a convenience. Which directories make up that set is the
6
- * client's decision — it declares them as MCP roots, and {@link Workspace} only asks. What
6
+ * client's decision - it declares them as MCP roots, and {@link Workspace} only asks. What
7
7
  * the workspace enforces is that everything goes through `resolve`, which refuses anything
8
8
  * landing outside all of them, including by way of `..`, an absolute path, or a symlink
9
9
  * pointing out of the tree.
@@ -84,7 +84,7 @@ export class Workspace {
84
84
  *
85
85
  * A root is resolved once and remembered, so a root that is itself a link still compares
86
86
  * equal to the paths resolved beneath it. Roots that do not exist are dropped rather than
87
- * fatal — a client may well declare a directory this server has no business in — and only
87
+ * fatal - a client may well declare a directory this server has no business in - and only
88
88
  * an empty result is an error.
89
89
  *
90
90
  * @returns {Promise<Root[]>} The roots, in the order the client gave them.
@@ -124,7 +124,7 @@ export class Workspace {
124
124
  if (!roots.length) {
125
125
  throw new WorkspaceError(
126
126
  refused.length
127
- ? `no usable workspace root — ${refused.join('; ')}`
127
+ ? `no usable workspace root - ${refused.join('; ')}`
128
128
  : 'the client has declared no workspace root, so there is nowhere to read or write; '
129
129
  + 'declare one with the MCP roots capability',
130
130
  );
@@ -185,7 +185,7 @@ export class Workspace {
185
185
  // the caller gave is used to choose segments and never to name a file. Each segment has
186
186
  // to match SAFE_SEGMENT, and what comes out is assembled onto a directory the client
187
187
  // declared. Every line below, and every caller, works with that value rather than the one
188
- // that came in — so the walk beneath cannot be steered by a name this never approved.
188
+ // that came in - so the walk beneath cannot be steered by a name this never approved.
189
189
  const base = this.#baseOf(root, lexical);
190
190
  const segments = relative(base, lexical).split(sep).filter(Boolean);
191
191
  if (!segments.every((segment) => SAFE_SEGMENT.test(segment))) {
@@ -193,7 +193,7 @@ export class Workspace {
193
193
  }
194
194
  const absolute = segments.reduce((at, segment) => join(at, segment), base);
195
195
 
196
- // The file itself may not exist yet — a write is the common case — so the deepest
196
+ // The file itself may not exist yet - a write is the common case - so the deepest
197
197
  // ancestor that does exist is what gets its links followed. This is the one check that
198
198
  // cannot be made without the filesystem: whether a directory inside a root is a link out
199
199
  // of it is not answerable from the path alone.
@@ -208,7 +208,7 @@ export class Workspace {
208
208
  }
209
209
 
210
210
  /**
211
- * Which spelling of a root a path sits under — the one the client gave, or the one symlinks
211
+ * Which spelling of a root a path sits under - the one the client gave, or the one symlinks
212
212
  * resolve it to.
213
213
  *
214
214
  * @param {Root} root The root holding it.
@@ -294,7 +294,7 @@ export class Workspace {
294
294
 
295
295
  const replaced = existsSync(absolute);
296
296
  if (replaced && !overwrite) {
297
- throw new WorkspaceError(`"${path}" already exists — pass overwrite: true to replace it`);
297
+ throw new WorkspaceError(`"${path}" already exists - pass overwrite: true to replace it`);
298
298
  }
299
299
 
300
300
  await mkdir(dirname(absolute), { recursive: true });