@rse/specbook 1.3.0 → 1.3.2

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.
@@ -142,11 +142,11 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
142
142
  };
143
143
  /* embed and re-base on clones of the objects (as the AST is shared
144
144
  with the other exports), moving the diagrams along */
145
- const localized = (object, from, to) => {
145
+ const embedObjectMd = (object, from, to) => {
146
146
  const clone = {
147
147
  ...object,
148
148
  properties: object.properties.map((property) => ({ ...property, value: embedMd(property.value, property.embedding ?? [], from, to) })),
149
- children: object.children.map((child) => localized(child, from, to))
149
+ children: object.children.map((child) => embedObjectMd(child, from, to))
150
150
  };
151
151
  if (object.description !== undefined)
152
152
  clone.description = { ...object.description,
@@ -160,7 +160,7 @@ export const renderMarkdown = async (specification, config, rebase, verbose) =>
160
160
  const origin = rebase?.origins.get(artifact);
161
161
  const from = origin !== undefined ? path.resolve(path.dirname(origin)) : undefined;
162
162
  const to = rebase !== undefined ? path.resolve(rebase.dir) : undefined;
163
- return artifact.objects.map((object) => localized(object, from, to));
163
+ return artifact.objects.map((object) => embedObjectMd(object, from, to));
164
164
  });
165
165
  const definitions = Array.from(labels, ([url, label]) => `[${label}]: ${url}\n`).join("");
166
166
  /* the single frontmatter block (only recognized at the start of a
@@ -3,7 +3,7 @@
3
3
  ** Copyright (c) 2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
4
4
  ** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
5
5
  */
6
- import fs from "node:fs";
6
+ import * as fs from "node:fs";
7
7
  import { escapeHtml, paperSetup, paperLength, paperSizeDefault } from "./specbook-export-common.js";
8
8
  import { literal } from "./specbook-verbose.js";
9
9
  /* extract the per-anchor page numbers from a Chromium-generated PDF,
@@ -130,9 +130,10 @@ const drawBrandBar = async (doc, accent) => {
130
130
  height: page.getHeight(), color: rgb(r, g, b) });
131
131
  };
132
132
  /* the launch options of the Chromium-class browser printing the PDF,
133
- resolved just once per process, as probing a system-installed Google
134
- Chrome has to actually launch it and as the export preflight and the
135
- renderer both ask for the very same browser */
133
+ resolved just once per process (a failed resolution excepted, so a
134
+ long-running process retries after the remedy), as probing a
135
+ system-installed Google Chrome has to actually launch it and as the
136
+ export preflight and the renderer both ask for the very same browser */
136
137
  let browserOptions;
137
138
  /* the remedy of an unavailable Playwright Chromium, shared by the
138
139
  fallback notice and the missing-browser error, as both have to point
@@ -191,10 +192,15 @@ const resolveBrowser = async (verbose) => {
191
192
  inside the rendering and with a Playwright-internal message */
192
193
  export const requireBrowser = async (verbose) => {
193
194
  browserOptions ??= resolveBrowser(verbose);
194
- const options = await browserOptions;
195
- if (options === undefined)
195
+ const options = await browserOptions.catch((err) => {
196
+ browserOptions = undefined;
197
+ throw err;
198
+ });
199
+ if (options === undefined) {
200
+ browserOptions = undefined;
196
201
  throw new Error("the PDF export requires a Chromium-class browser, but neither the " +
197
202
  `Playwright Chromium nor a system-installed Google Chrome was found -- ${chromiumRemedy}`);
203
+ }
198
204
  return options;
199
205
  };
200
206
  /* the bound of the ToC page number fixpoint iteration, as the page
@@ -12,9 +12,12 @@ import picomatch from "picomatch";
12
12
  honoring the anchored-vs-floating, directory-only, and negation
13
13
  semantics of the pattern; an anchored pattern resolves relative to
14
14
  the "base" directory its rule file governs, given as a POSIX path
15
- relative to the working tree root */
16
- const compileRule = (line, base) => {
17
- let pattern = line.trim();
15
+ relative to the working tree root, and "nocase" folds the case of
16
+ the pattern like Git does under "core.ignoreCase" */
17
+ const compileRule = (line, base, nocase) => {
18
+ /* Git drops the trailing whitespace only (unless a backslash
19
+ escapes it), while a leading one is part of the pattern */
20
+ let pattern = line.replace(/(?<!\\)\s+$/, "");
18
21
  if (pattern === "" || pattern.startsWith("#"))
19
22
  return null;
20
23
  let negated = false;
@@ -27,20 +30,28 @@ const compileRule = (line, base) => {
27
30
  dirOnly = true;
28
31
  pattern = pattern.slice(0, -1);
29
32
  }
33
+ /* a trailing "/**" matches everything inside the directory but not
34
+ the directory itself (so a later negation can still re-include a
35
+ file below it), whereas picomatch would match the directory, too */
36
+ if (pattern.endsWith("/**"))
37
+ pattern += "/*";
30
38
  /* a pattern carrying a slash anywhere but at its end is anchored at
31
39
  the directory of its rule file, while every other one floats and
32
40
  hence matches at any depth below it */
33
41
  const anchored = pattern.includes("/");
34
42
  if (pattern.startsWith("/"))
35
43
  pattern = pattern.slice(1);
44
+ /* a negated character class is "[!...]" in the wildmatch of Git,
45
+ while picomatch knows the "[^...]" spelling only */
46
+ pattern = pattern.replace(/\[!/g, "[^");
36
47
  const glob = anchored ? (base === "" ? pattern : `${base}/${pattern}`) : `**/${pattern}`;
37
- const isMatch = picomatch(glob, { dot: true, nobrace: true, noextglob: true, nonegate: true });
48
+ const isMatch = picomatch(glob, { dot: true, nobrace: true, noextglob: true, nonegate: true, nocase });
38
49
  return { matcher: isMatch, negated, dirOnly };
39
50
  };
40
51
  /* load the rules of a single exclude file, given as an absolute "file"
41
52
  and the working-tree-relative "base" its anchored patterns resolve
42
53
  against; an absent or unreadable file contributes no rules */
43
- const loadRules = (file, base) => {
54
+ const loadRules = (file, base, nocase) => {
44
55
  let text;
45
56
  try {
46
57
  text = fs.readFileSync(file, "utf8");
@@ -50,7 +61,7 @@ const loadRules = (file, base) => {
50
61
  }
51
62
  const rules = new Array();
52
63
  for (const line of text.split(/\r?\n/)) {
53
- const rule = compileRule(line, base);
64
+ const rule = compileRule(line, base, nocase);
54
65
  if (rule !== null)
55
66
  rules.push(rule);
56
67
  }
@@ -70,25 +81,28 @@ const workingTree = (dir) => {
70
81
  current = parent;
71
82
  }
72
83
  };
73
- /* resolve the global Git excludes file: the configured
74
- "core.excludesFile", or else the XDG location Git falls back onto
75
- when that configuration value is unset */
76
- const globalFile = (root) => {
77
- let file = "";
84
+ /* query a single Git configuration value of a working tree (its "~"
85
+ expanded and its booleans normalized by Git itself through the
86
+ "--type"), where an unset value or no Git at all yields "" */
87
+ const gitConfig = (root, type, key) => {
78
88
  try {
79
- file = execFileSync("git", ["config", "--get", "core.excludesFile"], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
89
+ return execFileSync("git", ["config", "--get", `--type=${type}`, key], { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
80
90
  }
81
91
  catch {
82
- /* no such configuration value, or no Git at all */
92
+ return "";
83
93
  }
94
+ };
95
+ /* resolve the global Git excludes file: the configured
96
+ "core.excludesFile", or else the XDG location Git falls back onto
97
+ when that configuration value is unset */
98
+ const globalFile = (root) => {
99
+ const file = gitConfig(root, "path", "core.excludesFile");
84
100
  if (file === "") {
85
101
  const xdg = process.env.XDG_CONFIG_HOME;
86
102
  return xdg !== undefined && xdg !== "" ?
87
103
  path.join(xdg, "git", "ignore") :
88
104
  path.join(os.homedir(), ".config", "git", "ignore");
89
105
  }
90
- if (file.startsWith("~/"))
91
- return path.join(os.homedir(), file.slice(2));
92
106
  return file;
93
107
  };
94
108
  /* resolve the repository-local exclude file of a working tree, held by
@@ -121,16 +135,19 @@ export const excluder = (dir) => {
121
135
  const root = workingTree(dir);
122
136
  if (root === null)
123
137
  return () => false;
138
+ /* Git folds the case of its exclude patterns on a case-insensitive
139
+ file system ("core.ignoreCase", set by Git itself on init/clone) */
140
+ const nocase = gitConfig(root, "bool", "core.ignoreCase") === "true";
124
141
  /* the rules which no directory of the working tree owns and which
125
142
  hence govern it as a whole, in ascending Git precedence */
126
- const base = [...loadRules(globalFile(root), ""), ...loadRules(infoFile(root), "")];
143
+ const baseRules = [...loadRules(globalFile(root), "", nocase), ...loadRules(infoFile(root), "", nocase)];
127
144
  /* the per-directory ".gitignore" rules, loaded on first use, as one
128
145
  run queries many files sharing the very same directories */
129
146
  const cache = new Map();
130
147
  const rulesOf = (relDir) => {
131
148
  let rules = cache.get(relDir);
132
149
  if (rules === undefined) {
133
- rules = loadRules(path.join(root, relDir, ".gitignore"), relDir);
150
+ rules = loadRules(path.join(root, relDir, ".gitignore"), relDir, nocase);
134
151
  cache.set(relDir, rules);
135
152
  }
136
153
  return rules;
@@ -147,7 +164,7 @@ export const excluder = (dir) => {
147
164
  directory excludes the file itself, as Git never descends
148
165
  into it -- and, for the same reason, a rule below such an
149
166
  ancestor can never re-include the file */
150
- let rules = base;
167
+ let rules = baseRules;
151
168
  let relDir = "";
152
169
  for (let i = 0; i < segments.length; i++) {
153
170
  rules = rules.concat(rulesOf(relDir));
@@ -141,8 +141,8 @@ const nearest = (index, matches, from) => {
141
141
  i++;
142
142
  return i;
143
143
  };
144
- const proximities = matches.map((object) => proximity(object));
145
- const best = proximities.reduce((max, p) => Math.max(max, p), 0);
144
+ const proximities = matches.map(proximity);
145
+ const best = Math.max(0, ...proximities);
146
146
  return matches.filter((_, i) => proximities[i] === best);
147
147
  };
148
148
  /* resolve a reference into a unique target: a single segment tries the
@@ -157,10 +157,11 @@ export const serveMcp = async (verbose) => {
157
157
  }
158
158
  }, async (args) => {
159
159
  try {
160
- const text = await specbook.describe({ ...args, compress: args.compress ?? 2 });
161
- if (args.output !== undefined && args.output !== "-") {
162
- await fs.promises.writeFile(outputOf(args, args.output), text);
163
- return { content: [{ type: "text", text: `described specification format into "${args.output}"` }] };
160
+ const { output, ...options } = args;
161
+ const text = await specbook.describe({ ...options, compress: options.compress ?? 2 });
162
+ if (output !== undefined && output !== "-") {
163
+ await fs.promises.writeFile(outputOf(args, output), text);
164
+ return { content: [{ type: "text", text: `described specification format into "${output}"` }] };
164
165
  }
165
166
  return { content: [{ type: "text", text }] };
166
167
  }
@@ -59,9 +59,12 @@ export const embeddingMimeType = (reference) => {
59
59
  /* the number of embedding entries an image embedding markup occupies,
60
60
  given its inline reference: one per variant of an embeddable one, none
61
61
  for any other one, and one for a reference-style markup (no reference) */
62
- export const embeddingCount = (reference) => reference === undefined ? 1 :
63
- (embeddingMimeType(reference.trim()) !== undefined ?
64
- embeddingVariants(reference.trim()).length : 0);
62
+ export const embeddingCount = (reference) => {
63
+ if (reference === undefined)
64
+ return 1;
65
+ const trimmed = reference.trim();
66
+ return embeddingMimeType(trimmed) !== undefined ? embeddingVariants(trimmed).length : 0;
67
+ };
65
68
  /* the image definition ("[label]: data:...") a reference-style image
66
69
  embedding refers to: a base64 data: URL of an embeddable image type */
67
70
  export const embeddingDataRegex = /^data:(image\/(?:svg\+xml|png|jpeg|webp));base64,([A-Za-z0-9+/=]+)$/;
@@ -108,7 +108,7 @@ const checkPropValue = (ctx, object, prop, expr, property, meta) => {
108
108
  checkLocal(ctx, object, prop, singleTarget(ctx, object, item), meta);
109
109
  }
110
110
  }
111
- else if (!directMatches(expr, plainText(property.value)))
111
+ else if (!directMatches(expr, plainText(property.value).trim()))
112
112
  ctx.diagnose(meta.file, meta.line, expr.kind === "enum" ?
113
113
  `property "${prop.name}" value "${property.value}" is not a member of "${prop.value}"` :
114
114
  `property "${prop.name}" value "${property.value}" does not match pattern "${prop.value}"`);
@@ -330,14 +330,13 @@ const checkAutomata = (ctx, schemas) => {
330
330
  the node set is skipped, as the property checks report it) */
331
331
  const succs = new Map(nodes.map((node) => [node, new Set()]));
332
332
  const preds = new Map(nodes.map((node) => [node, new Set()]));
333
- for (const edge of object.children.filter((child) => child.kind === automaton.edges)) {
334
- const source = referencedObjects(ctx, edge, automaton.source)[0];
335
- const target = referencedObjects(ctx, edge, automaton.target)[0];
336
- if (source !== undefined && target !== undefined && nodeSet.has(source) && nodeSet.has(target)) {
337
- succs.get(source)?.add(target);
338
- preds.get(target)?.add(source);
339
- }
340
- }
333
+ for (const edge of object.children.filter((child) => child.kind === automaton.edges))
334
+ for (const source of referencedObjects(ctx, edge, automaton.source))
335
+ for (const target of referencedObjects(ctx, edge, automaton.target))
336
+ if (nodeSet.has(source) && nodeSet.has(target)) {
337
+ succs.get(source)?.add(target);
338
+ preds.get(target)?.add(source);
339
+ }
341
340
  /* the transitive closure of a node set along a neighbor map */
342
341
  const closure = (starts, next) => {
343
342
  const seen = new Set(starts);
@@ -183,8 +183,9 @@ const parseList = (ctx, list, object, file, line) => {
183
183
  const property = { key: km[1].trim(), value };
184
184
  ctx.propMeta.set(property, { line });
185
185
  object.properties.push(property);
186
- if (item.tokens.some((sub) => sub.type === "list"))
187
- ctx.diagnose(file, line, `nested list below property "${property.key}" ignored`);
186
+ const ignored = item.tokens.find((sub) => sub.type !== "text" && sub.type !== "paragraph" && sub.type !== "space");
187
+ if (ignored !== undefined)
188
+ ctx.diagnose(file, line, `nested ${ignored.type} below property "${property.key}" ignored`);
188
189
  }
189
190
  else
190
191
  ctx.diagnose(file, line, `unrecognized list item "${first}"`);
@@ -269,13 +270,16 @@ const parseConcise = (ctx, item, parent, file, line, group) => {
269
270
  object.description = splitDescription(statements.join("; ").replace(/\.\s*$/, ""));
270
271
  /* recurse into nested concise-format child objects, tracking the
271
272
  source line of the nested list within the item (an ordered
272
- nested list is not supported, so it is reported, not lost) */
273
+ nested list or any other block content is not supported, so
274
+ it is reported, not lost) */
273
275
  let subLine = line;
274
276
  for (const sub of item.tokens) {
275
277
  if (sub.type === "list" && !sub.ordered)
276
278
  parseList(ctx, sub, object, file, subLine);
277
279
  else if (sub.type === "list")
278
280
  ctx.diagnose(file, subLine, `nested ordered list below ${kind} "${name}" ignored`);
281
+ else if (sub.type !== "text" && sub.type !== "paragraph" && sub.type !== "space")
282
+ ctx.diagnose(file, subLine, `nested ${sub.type} below ${kind} "${name}" ignored`);
279
283
  subLine += (sub.raw.match(/\n/g) ?? []).length;
280
284
  }
281
285
  parent.children.push(object);
@@ -304,6 +308,10 @@ const parseHeading = (ctx, token, state, stamps, file, line) => {
304
308
  state.stack.length = depth - 1;
305
309
  return;
306
310
  }
311
+ /* a level 1 object takes its parenthesized token as the id right
312
+ away, as the artifact resolution of the semantic phase matches
313
+ it against the configured id before any property could consume
314
+ the token (a nested object receives it there, see "validate") */
307
315
  const object = {
308
316
  kind: heading.kind,
309
317
  id: heading.id ?? (depth === 1 ? heading.paren : undefined) ?? slugify(heading.name),
@@ -337,7 +345,8 @@ const unsupportedTokens = {
337
345
  };
338
346
  /* parse a single source file into its artifacts */
339
347
  export const parseFile = (ctx, source) => {
340
- const { present, created, modified, body, offset } = parseFrontmatter(source.text);
348
+ /* strip a leading byte order mark, as it would hide the frontmatter */
349
+ const { present, created, modified, body, offset } = parseFrontmatter(source.text.replace(/^\uFEFF/, ""));
341
350
  if (!present)
342
351
  ctx.diagnose(source.file, 1, "missing frontmatter (\"Created:\"/\"Modified:\" block)");
343
352
  else
@@ -14,12 +14,22 @@ export const projectFile = ".specbook.yaml";
14
14
  /* the schema of the YAML project configuration: the files or glob
15
15
  patterns of the YAML schema configuration and the base directory
16
16
  of the specification Markdown files */
17
- const Project = v.strictObject({
17
+ const ProjectEntries = v.strictObject({
18
18
  config: v.optional(v.union([v.pipe(v.string(), v.minLength(1)),
19
19
  v.pipe(v.array(v.pipe(v.string(), v.minLength(1))), v.minLength(1))])),
20
20
  basedir: v.optional(v.pipe(v.string(), v.minLength(1)))
21
21
  }, (issue) => issue.expected === "never" ? "unknown project configuration entry" :
22
22
  "expected a map of project configuration entries");
23
+ /* whether a path names an existing regular file, where an
24
+ inaccessible one (not just an absent one) counts as absent */
25
+ const isFile = (file) => {
26
+ try {
27
+ return fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ };
23
33
  /* find the project configuration file in the closest ancestor-or-self
24
34
  of the directory (up to the filesystem root, as the file can reside
25
35
  even above a nested Git working tree) */
@@ -27,7 +37,7 @@ const findProject = (dir) => {
27
37
  let current = path.resolve(dir);
28
38
  for (;;) {
29
39
  const file = path.join(current, projectFile);
30
- if (fs.statSync(file, { throwIfNoEntry: false })?.isFile() === true)
40
+ if (isFile(file))
31
41
  return file;
32
42
  const parent = path.dirname(current);
33
43
  if (parent === current)
@@ -58,6 +68,7 @@ export const loadProject = (dir) => {
58
68
  const file = findProject(dir);
59
69
  if (file === undefined)
60
70
  return undefined;
71
+ /* parse the YAML, reporting its syntax errors as positioned diagnostics */
61
72
  const diagnostics = new Array();
62
73
  const lines = new LineCounter();
63
74
  const yaml = readProject(file);
@@ -71,7 +82,7 @@ export const loadProject = (dir) => {
71
82
  message: "invalid YAML syntax: " + err.message.replace(/ at line \d+, column \d+(?::\n[\s\S]*)?$/, "")
72
83
  });
73
84
  /* an empty or comment-only file carries no entries at all */
74
- const result = v.safeParse(Project, doc.errors.length === 0 ? (doc.toJS() ?? {}) : {});
85
+ const result = v.safeParse(ProjectEntries, doc.errors.length === 0 ? (doc.toJS() ?? {}) : {});
75
86
  if (!result.success)
76
87
  for (const issue of result.issues) {
77
88
  const keys = (issue.path ?? []).map((item) => item.key);
@@ -79,9 +90,11 @@ export const loadProject = (dir) => {
79
90
  diagnostics.push({ file, ...lineColOfPath(doc, lines, keys), severity: "error",
80
91
  message: `invalid project configuration: ${where}${issue.message}` });
81
92
  }
93
+ /* (the second condition just narrows the result type below) */
82
94
  if (diagnostics.length > 0 || !result.success)
83
95
  throw new Error("invalid project configuration:\n" +
84
96
  diagnostics.map(renderDiagnostic).join("\n"));
97
+ /* resolve the relative paths against the directory of the file */
85
98
  const base = path.dirname(file);
86
99
  const { config, basedir } = result.output;
87
100
  const patterns = typeof config === "string" ? [config] : config;
@@ -9,7 +9,11 @@ import { parse, generate } from "@rse/mrcs";
9
9
  and a signal spread derived from the second tone (if given) or
10
10
  else from the complement-transposed first tone */
11
11
  export const themeColors = (tone) => {
12
- const [accent, signal = `${accent}^`] = tone.trim().split(/\s+/);
12
+ const tones = tone.trim().split(/\s+/);
13
+ if (tones.length > 2 || !tones.every((t) => (/^#[0-9a-fA-F]{6}$/).test(t)))
14
+ throw new Error(`invalid theme color tone "${tone}" (expected "#RRGGBB", ` +
15
+ "optionally followed by a second one)");
16
+ const [accent, signal = `${accent}^`] = tones;
13
17
  return {
14
18
  base: generate(parse("#000000/32")),
15
19
  accent: generate(parse(`${accent}+40-5/32`)),
@@ -27,7 +31,7 @@ export const themeStylesheet = (colors) => ":root {\n" +
27
31
  aligned with the layer-2 CSS mapping of the HTML rendering */
28
32
  export const themeMapping = (colors, style) => {
29
33
  const idx = style === "dark" ?
30
- { muted: 22, symbol: 20, border: 16, accent: 24 } :
34
+ { muted: 22, symbol: 20, border: 16, accent: 22 } :
31
35
  { muted: 22, symbol: 24, border: 28, accent: 12 };
32
36
  return {
33
37
  muted: colors.base[idx.muted - 1],
@@ -15,10 +15,11 @@ export const parseVerbosity = (value) => {
15
15
  const level = (/^(?:true|yes|on)$/i).test(text) ? 1 :
16
16
  (/^(?:false|no|off)$/i).test(text) ? 0 :
17
17
  (/^\d+$/).test(text) ? Number(text) : NaN;
18
- if (!verbosities.includes(level))
18
+ const verbosity = verbosities.find((n) => n === level);
19
+ if (verbosity === undefined)
19
20
  throw new Error(`unknown verbosity "${value}" ` +
20
21
  `(supported: ${verbosities.join(", ")})`);
21
- return level;
22
+ return verbosity;
22
23
  };
23
24
  /* the sentinel characters delimiting a marked literal value, taken
24
25
  from the Unicode private use area, so they never clash with the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rse/specbook",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "publishConfig": { "access": "public" },
5
5
  "description": "Markdown-based Specification Format (API, CLI and MCP service)",
6
6
  "license": "Apache-2.0",
@@ -17,13 +17,13 @@
17
17
  },
18
18
  "files": [ "dst" ],
19
19
  "dependencies": {
20
- "marked": "18.0.13",
20
+ "marked": "18.0.14",
21
21
  "marked-smartypants": "1.1.12",
22
22
  "yaml": "2.9.1",
23
23
  "json5": "2.2.3",
24
24
  "pdf-lib": "1.17.1",
25
- "subset-font": "2.7.0",
26
- "sharp": "0.35.4",
25
+ "subset-font": "2.9.0",
26
+ "sharp": "0.35.5",
27
27
  "svgo": "4.1.0",
28
28
  "@rse/mrcs": "0.9.2",
29
29
  "@rse/gradia": "1.4.2",
@@ -32,36 +32,36 @@
32
32
  "nunjucks": "3.2.4",
33
33
  "@rse/nunjucks-addons": "2.2.0",
34
34
  "textframe": "1.2.1",
35
- "@swc/html": "1.16.2",
35
+ "@swc/html": "1.16.12",
36
36
  "tokenizr": "2.2.1",
37
37
  "playwright": "1.63.0",
38
38
  "source-code-error": "1.2.0",
39
39
  "commander": "15.0.0",
40
- "chalk": "6.0.0",
40
+ "chalk": "6.0.1",
41
41
  "chokidar5": "npm:chokidar@5.0.0",
42
42
  "glob": "13.0.6",
43
43
  "picomatch": "4.0.7",
44
44
  "es-toolkit": "1.52.0",
45
45
  "fastify": "5.12.5",
46
46
  "@fastify/websocket": "11.3.1",
47
- "@modelcontextprotocol/sdk": "1.30.0",
47
+ "@modelcontextprotocol/sdk": "1.31.0",
48
48
  "zod": "4.6.5"
49
49
  },
50
50
  "devDependencies": {
51
51
  "chokidar-cli": "3.0.0",
52
52
  "eslint": "10.9.0",
53
53
  "@eslint/js": "10.0.1",
54
- "typescript-eslint": "8.70.0",
54
+ "typescript-eslint": "8.71.0",
55
55
  "eslint-plugin-promise": "7.3.0",
56
56
  "neostandard": "0.14.0-next.1",
57
57
  "globals": "17.12.0",
58
58
  "markdownlint": "0.41.1",
59
59
  "markdownlint-cli2": "0.23.3",
60
- "@types/node": "26.6.2",
60
+ "@types/node": "26.6.3",
61
61
  "@types/picomatch": "4.0.3",
62
62
  "@types/subset-font": "2.3.0",
63
63
  "@types/nunjucks": "3.2.6",
64
- "@types/ws": "8.18.1",
64
+ "@types/ws": "8.18.2",
65
65
  "typescript": "6.0.3",
66
66
  "tsx": "4.23.15",
67
67
  "source-sans": "3.52.0",