@fruggr/zendesk-mcp-server 2.17.2 → 2.19.0

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 (2) hide show
  1. package/dist/index.js +304 -31
  2. package/package.json +8 -8
package/dist/index.js CHANGED
@@ -281,7 +281,6 @@ const startBrowserAuth = (config, logger = silentLogger) => {
281
281
  });
282
282
  const requestedPort = config.callbackPort ?? 27439;
283
283
  const onStartError = (err) => {
284
- clearTimeout(authTimeout);
285
284
  const code = err.code;
286
285
  logger.error("oauth_callback_listen_failed", {
287
286
  port: requestedPort,
@@ -812,26 +811,195 @@ const loadConfig = (argv = process.argv.slice(2)) => {
812
811
  });
813
812
  };
814
813
  //#endregion
814
+ //#region src/client/retry.ts
815
+ const MAX_ATTEMPTS = 3;
816
+ const BASE_DELAY_MS = 250;
817
+ const MAX_DELAY_MS = 4e3;
818
+ const REQUEST_TIMEOUT_MS = 3e4;
819
+ const TRANSFER_TIMEOUT_MS = 12e4;
820
+ /**
821
+ * A deadline for one attempt. The abort surfaces as a `TimeoutError` whose `code`
822
+ * is numeric, not a syscall string, so it classifies as `unknown`: a GET is
823
+ * retried, a write is not — the request may have arrived before the deadline.
824
+ */
825
+ const deadlineSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs);
826
+ const MAX_RETRY_AFTER_MS = 5e3;
827
+ const POLICIES = {
828
+ GET: {
829
+ network: "any",
830
+ serverErrors: true
831
+ },
832
+ DELETE: {
833
+ network: "pre-send",
834
+ serverErrors: false
835
+ },
836
+ POST: {
837
+ network: "pre-send",
838
+ serverErrors: false
839
+ },
840
+ PUT: {
841
+ network: "pre-send",
842
+ serverErrors: false
843
+ }
844
+ };
845
+ const DELAY_SECONDS = /^\d+$/;
846
+ const HTTP_DATE_START = /^[A-Za-z]/;
847
+ const NON_ASCII = /[^ -~]/g;
848
+ const TOKEN_SEGMENT = /\/token\/[^/]+/;
849
+ const URL_IN_TEXT = /[a-z][a-z0-9+.-]*:\/\/\S+/gi;
850
+ const PRE_SEND_CODES = /* @__PURE__ */ new Set([
851
+ "ENOTFOUND",
852
+ "EAI_AGAIN",
853
+ "ECONNREFUSED",
854
+ "UND_ERR_CONNECT_TIMEOUT"
855
+ ]);
856
+ const defaultRetryDeps = {
857
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
858
+ random: Math.random
859
+ };
860
+ /** Non-ASCII bytes break `node:http` headers, so client error text stays ASCII. */
861
+ const toAscii = (text) => text.replace(NON_ASCII, "?");
862
+ /**
863
+ * Identifies the request without leaking a credential: uploads carry an upload
864
+ * token in the query string, and an attachment `content_url` carries a download
865
+ * token as a path segment (`/attachments/token/<token>/`).
866
+ */
867
+ const describeTarget = (url) => {
868
+ const { origin, pathname } = new URL(url);
869
+ return `${origin}${pathname.replace(TOKEN_SEGMENT, "/token/[redacted]")}`;
870
+ };
871
+ /** First `code` in the cause chain, inspecting 5 levels so a cycle cannot hang. */
872
+ const errorCode = (err) => {
873
+ let current = err;
874
+ for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth += 1) {
875
+ const { code, cause } = current;
876
+ if (typeof code === "string") return code;
877
+ current = cause;
878
+ }
879
+ };
880
+ const classifyNetworkError = (err) => PRE_SEND_CODES.has(errorCode(err)) ? "pre-send" : "unknown";
881
+ const UNINFORMATIVE_NAMES = /* @__PURE__ */ new Set(["Error", "TypeError"]);
882
+ /**
883
+ * The client refuses to replay a write that may have landed — but the caller is
884
+ * an LLM whose reflex on a failed tool call is to try again, which would undo
885
+ * that. So a failed write says whether it may have taken effect. ASCII only,
886
+ * same rule as the rest of the message.
887
+ */
888
+ const MAY_HAVE_APPLIED_NOTE = "The write may already have been applied. Check the current state before retrying, or you may duplicate it.";
889
+ const NEVER_SENT_NOTE = "The request never reached Zendesk, so nothing was applied. Retrying is safe.";
890
+ const REFUSED_NOTE = "Zendesk refused the request, so nothing was applied. Retrying is safe.";
891
+ /** Only a write can be duplicated by a replay, so only a write carries a note. */
892
+ const writeNote = (method, note) => method === "GET" ? "" : ` ${note}`;
893
+ /**
894
+ * A cause message can quote the URL it failed on — Node's `fetch` refuses a URL
895
+ * carrying credentials by printing the whole thing, query string included — which
896
+ * would smuggle back exactly what `describeTarget` drops. Same treatment for both,
897
+ * so one function owns what a URL may look like in our output.
898
+ */
899
+ const redactUrls = (text) => text.replace(URL_IN_TEXT, (found) => {
900
+ try {
901
+ return new URL(found).origin === "null" ? "[url]" : describeTarget(found);
902
+ } catch {
903
+ return "[url]";
904
+ }
905
+ });
906
+ const failureDetail = (cause) => {
907
+ if (!(cause instanceof Error)) return redactUrls(String(cause));
908
+ const message = redactUrls(cause.message);
909
+ const code = errorCode(cause);
910
+ if (code !== void 0) return `${code}: ${message}`;
911
+ return UNINFORMATIVE_NAMES.has(cause.name) ? message : `${cause.name}: ${message}`;
912
+ };
913
+ const networkErrorMessage = (method, target, attempts, cause) => {
914
+ const base = `Network error on ${method} ${target} after ${attempts === 1 ? "1 attempt" : `${attempts} attempts`}: ${failureDetail(cause)}`;
915
+ const note = classifyNetworkError(cause) === "pre-send" ? NEVER_SENT_NOTE : MAY_HAVE_APPLIED_NOTE;
916
+ return toAscii(method === "GET" ? base : `${base}.${writeNote(method, note)}`);
917
+ };
918
+ const createZendeskNetworkError = (method, target, attempts, cause) => Object.assign(new Error(networkErrorMessage(method, target, attempts, cause), { cause }), {
919
+ name: "ZendeskNetworkError",
920
+ method,
921
+ target,
922
+ attempts
923
+ });
924
+ /** Exponential backoff with equal jitter: half the window fixed, half random. */
925
+ const computeBackoffMs = (attempt, random) => {
926
+ const window = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
927
+ return Math.round(window / 2 + window * random() / 2);
928
+ };
929
+ /** `Retry-After` in ms — delay-seconds or HTTP-date. */
930
+ const parseRetryAfter = (header, now = Date.now()) => {
931
+ if (header === null) return void 0;
932
+ const value = header.trim();
933
+ if (DELAY_SECONDS.test(value)) return Number(value) * 1e3;
934
+ if (!HTTP_DATE_START.test(value)) return void 0;
935
+ const date = Date.parse(value);
936
+ if (Number.isNaN(date)) return void 0;
937
+ return Math.max(0, date - now);
938
+ };
939
+ /**
940
+ * How long to wait before replaying this response, or undefined to accept it.
941
+ * `Retry-After` wins over backoff wherever it appears — Zendesk sends it on a 503
942
+ * during maintenance as well as on a 429 — and a value past the cap means the
943
+ * response is surfaced rather than parking the call for that long.
944
+ */
945
+ const retryDelayFor = (response, policy, attempt, random) => {
946
+ if (!(response.status === 429 || response.status >= 500 && policy.serverErrors)) return void 0;
947
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
948
+ if (retryAfter === void 0) return computeBackoffMs(attempt, random);
949
+ return retryAfter > MAX_RETRY_AFTER_MS ? void 0 : retryAfter;
950
+ };
951
+ /**
952
+ * Runs `attempt` until it succeeds, hits a terminal outcome, or spends the
953
+ * attempt budget. Returns the last response for the caller to inspect (a
954
+ * non-ok status is still the caller's to turn into a `ZendeskApiError`), and
955
+ * throws `ZendeskNetworkError` when no response was ever obtained.
956
+ */
957
+ const fetchWithRetry = async (attempt, method, target, deps = defaultRetryDeps) => {
958
+ const policy = POLICIES[method];
959
+ for (let tries = 1;; tries += 1) {
960
+ const last = tries >= MAX_ATTEMPTS;
961
+ let response;
962
+ try {
963
+ response = await attempt();
964
+ } catch (err) {
965
+ const replayable = policy.network === "any" || classifyNetworkError(err) === policy.network;
966
+ if (last || !replayable) throw createZendeskNetworkError(method, target, tries, err);
967
+ await deps.sleep(computeBackoffMs(tries, deps.random));
968
+ continue;
969
+ }
970
+ if (last) return response;
971
+ const delayMs = retryDelayFor(response, policy, tries, deps.random);
972
+ if (delayMs === void 0) return response;
973
+ await response.text().catch(() => void 0);
974
+ await deps.sleep(delayMs);
975
+ }
976
+ };
977
+ //#endregion
815
978
  //#region src/client/zendesk-api.ts
816
979
  var ZendeskApiError = class ZendeskApiError extends Error {
817
980
  status;
818
981
  statusText;
819
982
  body;
820
- constructor(status, statusText, body) {
821
- super(ZendeskApiError.buildMessage(status, statusText, body));
983
+ method;
984
+ constructor(status, statusText, body, method) {
985
+ super(ZendeskApiError.buildMessage(status, statusText, body, method));
822
986
  this.status = status;
823
987
  this.statusText = statusText;
824
988
  this.body = body;
989
+ this.method = method;
825
990
  this.name = "ZendeskApiError";
826
991
  }
827
- static buildMessage(status, statusText, body) {
992
+ static buildMessage(status, statusText, body, method) {
828
993
  switch (status) {
829
994
  case 401: return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
830
995
  case 403: return "Permission denied. Your Zendesk account does not have access to this resource.";
831
996
  case 404: return `Resource not found. Please verify the ID is correct. (${statusText})`;
832
997
  case 422: return `Validation error: ${body}`;
833
- case 429: return "Rate limit exceeded. Please wait before making more requests.";
834
- default: return `Zendesk API error ${status}: ${statusText}. ${body}`;
998
+ case 429: return `Rate limit exceeded. Please wait before making more requests.${writeNote(method, REFUSED_NOTE)}`;
999
+ default: {
1000
+ const message = `Zendesk API error ${status}: ${statusText}. ${body}`;
1001
+ return status >= 500 ? `${message}${writeNote(method, MAY_HAVE_APPLIED_NOTE)}` : message;
1002
+ }
835
1003
  }
836
1004
  }
837
1005
  };
@@ -841,6 +1009,11 @@ const buildUrl = (base, path, params) => {
841
1009
  if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
842
1010
  return url.toString();
843
1011
  };
1012
+ const performFetch = (method, url, init, timeoutMs = REQUEST_TIMEOUT_MS) => fetchWithRetry(() => fetch(url, {
1013
+ ...init,
1014
+ method,
1015
+ signal: deadlineSignal(timeoutMs)
1016
+ }), method, describeTarget(url));
844
1017
  const executeRequest = async (url, token, options = {}) => {
845
1018
  const { method = "GET", body } = options;
846
1019
  const headers = {
@@ -848,15 +1021,12 @@ const executeRequest = async (url, token, options = {}) => {
848
1021
  Accept: "application/json"
849
1022
  };
850
1023
  if (body) headers["Content-Type"] = "application/json";
851
- const init = {
852
- method,
853
- headers
854
- };
1024
+ const init = { headers };
855
1025
  if (body) init.body = JSON.stringify(body);
856
- const response = await fetch(url, init);
1026
+ const response = await performFetch(method, url, init);
857
1027
  if (!response.ok) {
858
1028
  const responseBody = await response.text();
859
- throw new ZendeskApiError(response.status, response.statusText, responseBody);
1029
+ throw new ZendeskApiError(response.status, response.statusText, responseBody, method);
860
1030
  }
861
1031
  if (response.status === 204) return {};
862
1032
  return response.json();
@@ -905,10 +1075,10 @@ const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
905
1075
  const expectedHost = `${subdomain}.zendesk.com`;
906
1076
  const headers = {};
907
1077
  if (new URL(contentUrl).hostname === expectedHost) headers["Authorization"] = buildAuthHeader(token);
908
- const response = await fetch(contentUrl, { headers });
1078
+ const response = await performFetch("GET", contentUrl, { headers }, TRANSFER_TIMEOUT_MS);
909
1079
  if (!response.ok) {
910
1080
  const body = await response.text();
911
- throw new ZendeskApiError(response.status, response.statusText, body);
1081
+ throw new ZendeskApiError(response.status, response.statusText, body, "GET");
912
1082
  }
913
1083
  const contentType = response.headers.get("content-type") ?? "application/octet-stream";
914
1084
  const arrayBuffer = await response.arrayBuffer();
@@ -921,30 +1091,28 @@ const zendeskUpload = async (subdomain, token, filename, data, contentType, uplo
921
1091
  const params = { filename };
922
1092
  if (uploadToken) params["token"] = uploadToken;
923
1093
  const url = buildUrl(getBaseUrl(subdomain), "/uploads", params);
924
- const response = await fetch(url, {
925
- method: "POST",
1094
+ const response = await performFetch("POST", url, {
926
1095
  headers: {
927
1096
  Authorization: buildAuthHeader(token),
928
1097
  "Content-Type": contentType
929
1098
  },
930
1099
  body: data
931
- });
1100
+ }, TRANSFER_TIMEOUT_MS);
932
1101
  if (!response.ok) {
933
1102
  const responseBody = await response.text();
934
- throw new ZendeskApiError(response.status, response.statusText, responseBody);
1103
+ throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
935
1104
  }
936
1105
  return response.json();
937
1106
  };
938
1107
  const helpCenterUpload = async (subdomain, token, path, formData) => {
939
1108
  const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
940
- const response = await fetch(url, {
941
- method: "POST",
1109
+ const response = await performFetch("POST", url, {
942
1110
  headers: { Authorization: buildAuthHeader(token) },
943
1111
  body: formData
944
- });
1112
+ }, TRANSFER_TIMEOUT_MS);
945
1113
  if (!response.ok) {
946
1114
  const responseBody = await response.text();
947
- throw new ZendeskApiError(response.status, response.statusText, responseBody);
1115
+ throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
948
1116
  }
949
1117
  return response.json();
950
1118
  };
@@ -4375,6 +4543,10 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
4375
4543
  * `reload_tools` tool that hot-reloads edited tool code on demand. stdio only —
4376
4544
  * HTTP builds a per-session server per request, so there is no long-lived
4377
4545
  * server to hot-swap.
4546
+ *
4547
+ * Returns the running server so the caller can close it on shutdown: dev mode
4548
+ * runs over the same stdio transport as normal mode and must exit the same way
4549
+ * when the client disconnects.
4378
4550
  */
4379
4551
  /* v8 ignore start -- runtime bootstrap: binds the reload tool to a real stdio
4380
4552
  transport; the reload machinery it wires up is covered by dev-reload.test.ts */
@@ -4383,6 +4555,7 @@ const startDevServer = async (config, getToken, logger = silentLogger, onUnautho
4383
4555
  registerReloadTool(server, reload, logger);
4384
4556
  await startStdioTransport(server, logger);
4385
4557
  logger.info("dev_mode_enabled");
4558
+ return server;
4386
4559
  };
4387
4560
  /* v8 ignore stop */
4388
4561
  //#endregion
@@ -4593,8 +4766,10 @@ const readJsonBody = (req, maxBodyBytes) => new Promise((resolve) => {
4593
4766
  const respondBodyError = (req, res, failure) => {
4594
4767
  const headers = failure.status === 413 ? { Connection: "close" } : {};
4595
4768
  sendJsonRpcError(res, failure.status, failure.rpcCode, failure.message, headers);
4596
- if (failure.status === 413) if (res.writableFinished) req.destroy();
4597
- else res.once("finish", () => req.destroy());
4769
+ if (failure.status === 413) {
4770
+ if (res.writableFinished) req.destroy();
4771
+ else res.once("finish", () => req.destroy());
4772
+ }
4598
4773
  };
4599
4774
  const SESSION_IDLE_TIMEOUT_MS = 18e5;
4600
4775
  const SESSION_SWEEP_INTERVAL_MS = 6e4;
@@ -4736,27 +4911,125 @@ const startHttpTransport = async (config, logger = silentLogger, options = {}) =
4736
4911
  };
4737
4912
  };
4738
4913
  //#endregion
4914
+ //#region src/utils/shutdown.ts
4915
+ /**
4916
+ * How long a shutdown may take before the watchdog forces the exit.
4917
+ *
4918
+ * Generous enough for `server.close()` and an HTTP session drain, short enough
4919
+ * to stay inside the tightest common supervisor grace — `docker stop` allows 10s
4920
+ * and Kubernetes 30s before their own SIGKILL (systemd is far laxer at 90s). A
4921
+ * process killed by its supervisor is exactly the unclean exit this removes.
4922
+ */
4923
+ const SHUTDOWN_GRACE_MS = 3e3;
4924
+ /**
4925
+ * Adapt a `process` to a {@link ShutdownRuntime}. Takes the process as an
4926
+ * argument rather than closing over the global so the adapter itself is
4927
+ * testable — otherwise the one part of this module that touches the real
4928
+ * process would be the one part no test can reach.
4929
+ */
4930
+ const createRuntime = (proc) => ({
4931
+ on: (event, listener) => {
4932
+ proc.on(event, listener);
4933
+ },
4934
+ stdin: { on: (event, listener) => {
4935
+ proc.stdin.on(event, listener);
4936
+ } },
4937
+ exit: (code) => {
4938
+ proc.exit(code);
4939
+ },
4940
+ setTimer: (fn, ms) => {
4941
+ const timer = setTimeout(fn, ms);
4942
+ return {
4943
+ unref: () => void timer.unref(),
4944
+ clear: () => clearTimeout(timer)
4945
+ };
4946
+ }
4947
+ });
4948
+ const defaultRuntime = createRuntime(process);
4949
+ /**
4950
+ * Install the process's one shutdown path and return its trigger.
4951
+ *
4952
+ * Registering a `SIGTERM` handler *removes* Node's default terminate, which
4953
+ * makes the exit our responsibility: a cleanup that stalls on an in-flight
4954
+ * request would otherwise leave a process SIGTERM cannot kill — the very
4955
+ * symptom this exists to remove. Hence the watchdog, which is load-bearing
4956
+ * rather than defensive, and the unconditional `exit` on every path.
4957
+ *
4958
+ * The exit is explicit rather than a drained event loop because the OAuth
4959
+ * callback server (`auth/browser-oauth.ts`) is a listening socket that is not
4960
+ * `unref()`'d: letting the loop drain would keep a disconnected session alive
4961
+ * for up to the 5-minute auth timeout.
4962
+ */
4963
+ const installShutdown = (options) => {
4964
+ const { cleanup, logger, watchStdin, graceMs = SHUTDOWN_GRACE_MS } = options;
4965
+ const runtime = options.runtime ?? defaultRuntime;
4966
+ let started = false;
4967
+ let exited = false;
4968
+ const exitOnce = (code) => {
4969
+ if (exited) return;
4970
+ exited = true;
4971
+ runtime.exit(code);
4972
+ };
4973
+ const shutdown = async (reason) => {
4974
+ if (started) return;
4975
+ started = true;
4976
+ logger.info("shutdown_started", { reason });
4977
+ const watchdog = runtime.setTimer(() => {
4978
+ logger.warn("shutdown_forced", { graceMs });
4979
+ exitOnce(0);
4980
+ }, graceMs);
4981
+ watchdog.unref();
4982
+ try {
4983
+ await cleanup();
4984
+ logger.info("shutdown_complete", { reason });
4985
+ } catch (err) {
4986
+ logger.warn("shutdown_cleanup_failed", { error: err instanceof Error ? err.message : String(err) });
4987
+ } finally {
4988
+ watchdog.clear();
4989
+ exitOnce(0);
4990
+ }
4991
+ };
4992
+ runtime.on("SIGINT", () => void shutdown("SIGINT"));
4993
+ runtime.on("SIGTERM", () => void shutdown("SIGTERM"));
4994
+ if (watchStdin) runtime.stdin.on("end", () => void shutdown("stdin_eof"));
4995
+ return shutdown;
4996
+ };
4997
+ //#endregion
4739
4998
  //#region src/index.ts
4740
4999
  const buildStdioTokenStore = (config, logger) => createTokenStore({
4741
5000
  subdomain: config.subdomain,
4742
5001
  oauthClientId: config.oauthClientId,
4743
5002
  callbackPort: config.callbackPort
4744
5003
  }, logger);
5004
+ const connectStdio = async (config, tokenStore, logger) => {
5005
+ if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
5006
+ const server = createMcpServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
5007
+ await startStdioTransport(server, logger);
5008
+ return server;
5009
+ };
4745
5010
  const main = async () => {
4746
5011
  const config = loadConfig();
4747
5012
  const logger = createLogger(config.logLevel);
4748
5013
  if (config.transport === "stdio") {
4749
5014
  const tokenStore = buildStdioTokenStore(config, logger);
4750
- if (config.dev) {
4751
- await startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
4752
- return;
4753
- }
4754
- const server = createMcpServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
4755
- await startStdioTransport(server, logger);
5015
+ const server = await connectStdio(config, tokenStore, logger);
5016
+ installShutdown({
5017
+ watchStdin: true,
5018
+ logger,
5019
+ cleanup: async () => {
5020
+ await server.close();
5021
+ tokenStore.dispose();
5022
+ }
5023
+ });
4756
5024
  return;
4757
5025
  }
4758
5026
  if (config.dev) logger.warn("dev_mode_ignored_http");
4759
- await startHttpTransport(config, logger);
5027
+ const http = await startHttpTransport(config, logger);
5028
+ installShutdown({
5029
+ watchStdin: false,
5030
+ logger,
5031
+ cleanup: http.close
5032
+ });
4760
5033
  };
4761
5034
  main().catch((error) => {
4762
5035
  console.error("Fatal error:", error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fruggr/zendesk-mcp-server",
3
- "version": "2.17.2",
3
+ "version": "2.19.0",
4
4
  "mcpName": "io.github.fruggr/zendesk-mcp-server",
5
5
  "description": "Deep Zendesk MCP server for your AI assistant: search, draft, update and translate Help Center articles and manage Support tickets end to end — comments, triage and image attachments.",
6
6
  "type": "module",
@@ -69,13 +69,13 @@
69
69
  "engines": {
70
70
  "node": ">=20"
71
71
  },
72
- "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
72
+ "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
73
73
  "dependencies": {
74
74
  "@modelcontextprotocol/sdk": "1.30.0",
75
75
  "cheerio": "1.2.0",
76
76
  "hast-util-to-html": "9.0.5",
77
77
  "hast-util-to-mdast": "10.1.2",
78
- "open": "11.0.0",
78
+ "open": "11.0.1",
79
79
  "rehype-parse": "9.0.1",
80
80
  "rehype-raw": "7.0.0",
81
81
  "rehype-remark": "10.0.1",
@@ -88,15 +88,15 @@
88
88
  "zod": "4.4.3"
89
89
  },
90
90
  "devDependencies": {
91
- "@biomejs/biome": "2.5.7",
92
- "@semantic-release/changelog": "^6.0.3",
91
+ "@biomejs/biome": "2.5.8",
92
+ "@semantic-release/changelog": "^7.0.0",
93
93
  "@semantic-release/exec": "^7.1.0",
94
- "@semantic-release/git": "^10.0.1",
94
+ "@semantic-release/git": "^11.0.0",
95
95
  "@semantic-release/github": "^12.0.6",
96
96
  "@semantic-release/npm": "^13.1.5",
97
97
  "@semantic-release/release-notes-generator": "^14.1.1",
98
- "@stryker-mutator/core": "^9.6.1",
99
- "@stryker-mutator/vitest-runner": "^9.6.1",
98
+ "@stryker-mutator/core": "^10.0.0",
99
+ "@stryker-mutator/vitest-runner": "^10.0.0",
100
100
  "@tsconfig/node20": "^20.1.9",
101
101
  "@tsconfig/strictest": "^2.0.8",
102
102
  "@types/hast": "^3.0.4",