@yawlabs/ssh-mcp 0.9.1 → 0.9.3

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/dist/index.js CHANGED
@@ -202,8 +202,9 @@ function checkSshConfig(host) {
202
202
  inHostBlock = patterns.some((p) => {
203
203
  if (p === "*") return true;
204
204
  if (p === host) return true;
205
- if (p.includes("*")) {
206
- const regex = new RegExp("^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
205
+ if (p.includes("*") || p.includes("?")) {
206
+ const escaped = p.replace(/[\\^$.|+()[\]{}]/g, "\\$&");
207
+ const regex = new RegExp("^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
207
208
  return regex.test(host);
208
209
  }
209
210
  return false;
@@ -339,9 +340,16 @@ function ensureAgent() {
339
340
  const result = probeAgent(sock, "ssh-agent");
340
341
  if (result) return result;
341
342
  }
342
- if (!sock && process.platform === "win32") {
343
+ if (process.platform === "win32") {
343
344
  const result = probeAgent("\\\\.\\pipe\\openssh-ssh-agent", "Windows OpenSSH agent");
344
345
  if (result) return result;
346
+ return {
347
+ running: false,
348
+ reachable: false,
349
+ keys: [],
350
+ started: false,
351
+ message: "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
352
+ };
345
353
  }
346
354
  const { stdout, ok } = runArgs("ssh-agent", ["-s"]);
347
355
  if (ok) {
@@ -369,7 +377,7 @@ function ensureAgent() {
369
377
  reachable: false,
370
378
  keys: [],
371
379
  started: false,
372
- message: process.platform === "win32" ? "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent" : 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
380
+ message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
373
381
  };
374
382
  }
375
383
  function detectKeyType(filePath, fileName) {
@@ -458,10 +466,10 @@ function loadKey(keyPath) {
458
466
  if (ok) {
459
467
  return { status: "ok", message: `Key loaded: ${resolved}` };
460
468
  }
461
- if (stdout.includes("passphrase") || stdout.includes("incorrect") || stdout.includes("bad permissions")) {
462
- if (stdout.includes("UNPROTECTED PRIVATE KEY")) {
463
- return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
464
- }
469
+ if (stdout.includes("UNPROTECTED PRIVATE KEY") || stdout.includes("too open") || stdout.includes("bad permissions")) {
470
+ return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
471
+ }
472
+ if (stdout.includes("passphrase") || stdout.includes("incorrect")) {
465
473
  return { status: "error", message: `Key ${resolved} requires a passphrase. Add it manually: ssh-add ${resolved}` };
466
474
  }
467
475
  return { status: "error", message: `Failed to load key: ${stdout}` };
@@ -749,10 +757,16 @@ async function connectWithProxy(resolved) {
749
757
  const jumpClient = await connectWithProxy(jumpResolved);
750
758
  const targetHost = resolved.connectConfig.host;
751
759
  const targetPort = resolved.connectConfig.port;
760
+ const endJump = () => {
761
+ try {
762
+ jumpClient.end();
763
+ } catch {
764
+ }
765
+ };
752
766
  const stream = await new Promise((resolve, reject) => {
753
767
  jumpClient.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream2) => {
754
768
  if (err) {
755
- jumpClient.end();
769
+ endJump();
756
770
  return reject(err);
757
771
  }
758
772
  resolve(stream2);
@@ -761,10 +775,10 @@ async function connectWithProxy(resolved) {
761
775
  return new Promise((resolve, reject) => {
762
776
  const client = new Client();
763
777
  client.on("ready", () => resolve(client)).on("error", (err) => {
764
- jumpClient.end();
778
+ endJump();
765
779
  reject(err);
766
780
  }).on("close", () => {
767
- jumpClient.end();
781
+ endJump();
768
782
  }).connect({ ...resolved.connectConfig, sock: stream });
769
783
  });
770
784
  }
@@ -832,14 +846,19 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
832
846
  stderrTruncated = true;
833
847
  }
834
848
  };
835
- stream.on("close", (code) => {
849
+ stream.on("close", (code, signal) => {
836
850
  let stdout = Buffer.concat(stdoutChunks).toString("utf8");
837
851
  let stderr = Buffer.concat(stderrChunks).toString("utf8");
838
852
  if (stdoutTruncated) stdout += `
839
853
  [output truncated at ${maxBytes} bytes]`;
840
854
  if (stderrTruncated) stderr += `
841
855
  [stderr truncated at ${maxBytes} bytes]`;
842
- settle(() => resolve({ stdout, stderr, code: code ?? 0 }));
856
+ const exitCode = typeof code === "number" ? code : -1;
857
+ const result = { stdout, stderr, code: exitCode };
858
+ if (stdoutTruncated) result.stdoutTruncated = true;
859
+ if (stderrTruncated) result.stderrTruncated = true;
860
+ if (signal) result.signal = signal;
861
+ settle(() => resolve(result));
843
862
  }).on("data", appendStdout).on("error", (err2) => {
844
863
  settle(() => reject(err2));
845
864
  });
@@ -936,6 +955,12 @@ async function listDir(client, remotePath) {
936
955
  }
937
956
 
938
957
  // src/pool.ts
958
+ function defaultMaxPoolSize() {
959
+ const raw = process.env.SSH_MCP_MAX_POOL_SIZE;
960
+ if (!raw) return 100;
961
+ const parsed = Number.parseInt(raw, 10);
962
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
963
+ }
939
964
  var ConnectionPool = class {
940
965
  entries = /* @__PURE__ */ new Map();
941
966
  // Coalesces concurrent connect attempts for the same key so we don't open N
@@ -952,7 +977,7 @@ var ConnectionPool = class {
952
977
  drained = false;
953
978
  constructor(options) {
954
979
  this.idleTtlMs = options?.idleTtlMs ?? 6e4;
955
- this.maxPoolSize = options?.maxPoolSize ?? 100;
980
+ this.maxPoolSize = options?.maxPoolSize ?? defaultMaxPoolSize();
956
981
  }
957
982
  async acquire(config) {
958
983
  const resolved = resolveConfig(config);
@@ -1202,6 +1227,7 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
1202
1227
  const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
1203
1228
  const pidMatch = raw.match(/Main PID:\s+(\d+)/);
1204
1229
  const sinceMatch = raw.match(/since\s+(.+?);/);
1230
+ const unknown = !activeMatch && result.code !== 0;
1205
1231
  const fallbackStatus = result.code === 0 ? "active" : "inactive";
1206
1232
  return {
1207
1233
  name: serviceName,
@@ -1210,7 +1236,8 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
1210
1236
  description: descMatch?.[1]?.trim(),
1211
1237
  since: sinceMatch?.[1]?.trim(),
1212
1238
  pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
1213
- raw
1239
+ raw,
1240
+ unknown
1214
1241
  };
1215
1242
  }
1216
1243
 
@@ -1247,6 +1274,7 @@ function registerTools(server, pool) {
1247
1274
  if (result.stdout) parts.push(result.stdout);
1248
1275
  if (result.stderr) parts.push(`[stderr]
1249
1276
  ${result.stderr}`);
1277
+ if (result.signal) parts.push(`[signal: ${result.signal}]`);
1250
1278
  parts.push(`[exit code: ${result.code}]`);
1251
1279
  return { content: [{ type: "text", text: parts.join("\n") }] };
1252
1280
  });
@@ -1523,11 +1551,12 @@ ${result.stderr}`);
1523
1551
  maxdepth: z.number().optional().describe("Maximum directory depth to search"),
1524
1552
  minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
1525
1553
  maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
1554
+ newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
1526
1555
  timeout: TimeoutSchema
1527
1556
  },
1528
- async ({ path, name, type, maxdepth, minsize, maxsize, timeout, ...conn }) => {
1557
+ async ({ path, name, type, maxdepth, minsize, maxsize, newer, timeout, ...conn }) => {
1529
1558
  return connectionPool.withConnection(conn, async (client) => {
1530
- const files = await find(client, { path, name, type, maxdepth, minsize, maxsize }, timeout || 3e4);
1559
+ const files = await find(client, { path, name, type, maxdepth, minsize, maxsize, newer }, timeout || 3e4);
1531
1560
  if (files.length === 0) {
1532
1561
  return { content: [{ type: "text", text: "No files found." }] };
1533
1562
  }
@@ -1542,7 +1571,7 @@ ${files.join("\n")}` }] };
1542
1571
  {
1543
1572
  ...connectionParams,
1544
1573
  path: z.string().describe("Absolute path to the file to tail"),
1545
- lines: z.number().optional().describe("Number of lines to read from the end (default: 100)"),
1574
+ lines: z.number().int().positive().optional().describe("Number of lines to read from the end (default: 100). Must be a positive integer."),
1546
1575
  grep: z.string().optional().describe("Case-insensitive pattern to filter lines"),
1547
1576
  timeout: TimeoutSchema
1548
1577
  },
@@ -1582,7 +1611,7 @@ ${files.join("\n")}` }] };
1582
1611
  if (status.since) lines.push(`Since: ${status.since}`);
1583
1612
  lines.push("");
1584
1613
  lines.push(status.raw);
1585
- return { content: [{ type: "text", text: lines.join("\n") }], isError: !status.active };
1614
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: status.unknown };
1586
1615
  });
1587
1616
  }
1588
1617
  );
package/dist/server.d.ts CHANGED
@@ -13,6 +13,12 @@ interface ExecResult {
13
13
  stdout: string;
14
14
  stderr: string;
15
15
  code: number;
16
+ /** True when stdout was truncated at the byte cap. */
17
+ stdoutTruncated?: boolean;
18
+ /** True when stderr was truncated at the byte cap. */
19
+ stderrTruncated?: boolean;
20
+ /** Signal name (e.g. "TERM") if the remote channel closed via signal instead of exit. */
21
+ signal?: string;
16
22
  }
17
23
  interface ResolvedConfig {
18
24
  connectConfig: ConnectConfig;
@@ -34,7 +40,13 @@ declare function listDir(client: Client, remotePath: string): Promise<string[]>;
34
40
  interface PoolOptions {
35
41
  /** Milliseconds before an idle connection is closed. Default: 60000 (60s) */
36
42
  idleTtlMs?: number;
37
- /** Maximum number of connections in the pool. Default: 100 */
43
+ /**
44
+ * Maximum number of connections in the pool. Default: 100, overridable via the
45
+ * `SSH_MCP_MAX_POOL_SIZE` env var. When at capacity, the pool first tries to evict
46
+ * an idle entry; if every entry is in use, `acquire()` rejects with
47
+ * "Connection pool is full". Bump this for fan-out workloads against many distinct
48
+ * hosts (e.g. `ssh_multi_exec` across a large fleet).
49
+ */
38
50
  maxPoolSize?: number;
39
51
  }
40
52
  declare class ConnectionPool {
@@ -163,6 +175,13 @@ interface ServiceStatus {
163
175
  since?: string;
164
176
  pid?: number;
165
177
  raw: string;
178
+ /**
179
+ * True when systemctl could not report on the unit at all: no `Active:` line
180
+ * parseable AND non-zero exit. Typical causes: typo'd unit name, unit file
181
+ * doesn't exist, systemd unreachable. Distinct from "service exists but is
182
+ * stopped" (active=false but unknown=false).
183
+ */
184
+ unknown: boolean;
166
185
  }
167
186
  declare function serviceStatus(client: Client, serviceName: string, timeoutMs?: number): Promise<ServiceStatus>;
168
187
 
package/dist/server.js CHANGED
@@ -200,8 +200,9 @@ function checkSshConfig(host) {
200
200
  inHostBlock = patterns.some((p) => {
201
201
  if (p === "*") return true;
202
202
  if (p === host) return true;
203
- if (p.includes("*")) {
204
- const regex = new RegExp("^" + p.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$");
203
+ if (p.includes("*") || p.includes("?")) {
204
+ const escaped = p.replace(/[\\^$.|+()[\]{}]/g, "\\$&");
205
+ const regex = new RegExp("^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$");
205
206
  return regex.test(host);
206
207
  }
207
208
  return false;
@@ -335,9 +336,16 @@ function ensureAgent() {
335
336
  const result = probeAgent(sock, "ssh-agent");
336
337
  if (result) return result;
337
338
  }
338
- if (!sock && process.platform === "win32") {
339
+ if (process.platform === "win32") {
339
340
  const result = probeAgent("\\\\.\\pipe\\openssh-ssh-agent", "Windows OpenSSH agent");
340
341
  if (result) return result;
342
+ return {
343
+ running: false,
344
+ reachable: false,
345
+ keys: [],
346
+ started: false,
347
+ message: "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent"
348
+ };
341
349
  }
342
350
  const { stdout, ok } = runArgs("ssh-agent", ["-s"]);
343
351
  if (ok) {
@@ -365,7 +373,7 @@ function ensureAgent() {
365
373
  reachable: false,
366
374
  keys: [],
367
375
  started: false,
368
- message: process.platform === "win32" ? "Windows OpenSSH agent not running. Start it: Get-Service ssh-agent | Set-Service -StartupType Automatic; Start-Service ssh-agent" : 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
376
+ message: 'Could not start ssh-agent. Run manually: eval "$(ssh-agent -s)"'
369
377
  };
370
378
  }
371
379
  function detectKeyType(filePath, fileName) {
@@ -454,10 +462,10 @@ function loadKey(keyPath) {
454
462
  if (ok) {
455
463
  return { status: "ok", message: `Key loaded: ${resolved}` };
456
464
  }
457
- if (stdout.includes("passphrase") || stdout.includes("incorrect") || stdout.includes("bad permissions")) {
458
- if (stdout.includes("UNPROTECTED PRIVATE KEY")) {
459
- return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
460
- }
465
+ if (stdout.includes("UNPROTECTED PRIVATE KEY") || stdout.includes("too open") || stdout.includes("bad permissions")) {
466
+ return { status: "error", message: `Key ${resolved} has too-open permissions. Fix: chmod 600 ${resolved}` };
467
+ }
468
+ if (stdout.includes("passphrase") || stdout.includes("incorrect")) {
461
469
  return { status: "error", message: `Key ${resolved} requires a passphrase. Add it manually: ssh-add ${resolved}` };
462
470
  }
463
471
  return { status: "error", message: `Failed to load key: ${stdout}` };
@@ -745,10 +753,16 @@ async function connectWithProxy(resolved) {
745
753
  const jumpClient = await connectWithProxy(jumpResolved);
746
754
  const targetHost = resolved.connectConfig.host;
747
755
  const targetPort = resolved.connectConfig.port;
756
+ const endJump = () => {
757
+ try {
758
+ jumpClient.end();
759
+ } catch {
760
+ }
761
+ };
748
762
  const stream = await new Promise((resolve, reject) => {
749
763
  jumpClient.forwardOut("127.0.0.1", 0, targetHost, targetPort, (err, stream2) => {
750
764
  if (err) {
751
- jumpClient.end();
765
+ endJump();
752
766
  return reject(err);
753
767
  }
754
768
  resolve(stream2);
@@ -757,10 +771,10 @@ async function connectWithProxy(resolved) {
757
771
  return new Promise((resolve, reject) => {
758
772
  const client = new Client();
759
773
  client.on("ready", () => resolve(client)).on("error", (err) => {
760
- jumpClient.end();
774
+ endJump();
761
775
  reject(err);
762
776
  }).on("close", () => {
763
- jumpClient.end();
777
+ endJump();
764
778
  }).connect({ ...resolved.connectConfig, sock: stream });
765
779
  });
766
780
  }
@@ -846,14 +860,19 @@ function exec(client, command, timeoutMs = 3e4, maxBytes = DEFAULT_MAX_EXEC_BYTE
846
860
  stderrTruncated = true;
847
861
  }
848
862
  };
849
- stream.on("close", (code) => {
863
+ stream.on("close", (code, signal) => {
850
864
  let stdout = Buffer.concat(stdoutChunks).toString("utf8");
851
865
  let stderr = Buffer.concat(stderrChunks).toString("utf8");
852
866
  if (stdoutTruncated) stdout += `
853
867
  [output truncated at ${maxBytes} bytes]`;
854
868
  if (stderrTruncated) stderr += `
855
869
  [stderr truncated at ${maxBytes} bytes]`;
856
- settle(() => resolve({ stdout, stderr, code: code ?? 0 }));
870
+ const exitCode = typeof code === "number" ? code : -1;
871
+ const result = { stdout, stderr, code: exitCode };
872
+ if (stdoutTruncated) result.stdoutTruncated = true;
873
+ if (stderrTruncated) result.stderrTruncated = true;
874
+ if (signal) result.signal = signal;
875
+ settle(() => resolve(result));
857
876
  }).on("data", appendStdout).on("error", (err2) => {
858
877
  settle(() => reject(err2));
859
878
  });
@@ -1019,6 +1038,7 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
1019
1038
  const descMatch = raw.match(/^\s+.*?-\s+(.+)$/m);
1020
1039
  const pidMatch = raw.match(/Main PID:\s+(\d+)/);
1021
1040
  const sinceMatch = raw.match(/since\s+(.+?);/);
1041
+ const unknown = !activeMatch && result.code !== 0;
1022
1042
  const fallbackStatus = result.code === 0 ? "active" : "inactive";
1023
1043
  return {
1024
1044
  name: serviceName,
@@ -1027,11 +1047,18 @@ async function serviceStatus(client, serviceName, timeoutMs = 3e4) {
1027
1047
  description: descMatch?.[1]?.trim(),
1028
1048
  since: sinceMatch?.[1]?.trim(),
1029
1049
  pid: pidMatch ? Number.parseInt(pidMatch[1], 10) : void 0,
1030
- raw
1050
+ raw,
1051
+ unknown
1031
1052
  };
1032
1053
  }
1033
1054
 
1034
1055
  // src/pool.ts
1056
+ function defaultMaxPoolSize() {
1057
+ const raw = process.env.SSH_MCP_MAX_POOL_SIZE;
1058
+ if (!raw) return 100;
1059
+ const parsed = Number.parseInt(raw, 10);
1060
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 100;
1061
+ }
1035
1062
  var ConnectionPool = class {
1036
1063
  entries = /* @__PURE__ */ new Map();
1037
1064
  // Coalesces concurrent connect attempts for the same key so we don't open N
@@ -1048,7 +1075,7 @@ var ConnectionPool = class {
1048
1075
  drained = false;
1049
1076
  constructor(options) {
1050
1077
  this.idleTtlMs = options?.idleTtlMs ?? 6e4;
1051
- this.maxPoolSize = options?.maxPoolSize ?? 100;
1078
+ this.maxPoolSize = options?.maxPoolSize ?? defaultMaxPoolSize();
1052
1079
  }
1053
1080
  async acquire(config) {
1054
1081
  const resolved = resolveConfig(config);
@@ -1252,6 +1279,7 @@ function registerTools(server, pool) {
1252
1279
  if (result.stdout) parts.push(result.stdout);
1253
1280
  if (result.stderr) parts.push(`[stderr]
1254
1281
  ${result.stderr}`);
1282
+ if (result.signal) parts.push(`[signal: ${result.signal}]`);
1255
1283
  parts.push(`[exit code: ${result.code}]`);
1256
1284
  return { content: [{ type: "text", text: parts.join("\n") }] };
1257
1285
  });
@@ -1528,11 +1556,12 @@ ${result.stderr}`);
1528
1556
  maxdepth: z.number().optional().describe("Maximum directory depth to search"),
1529
1557
  minsize: z.string().optional().describe("Minimum file size (e.g. '1M', '100k')"),
1530
1558
  maxsize: z.string().optional().describe("Maximum file size (e.g. '10M', '500k')"),
1559
+ newer: z.string().optional().describe("Reference file path -- find matches files modified more recently than this file"),
1531
1560
  timeout: TimeoutSchema
1532
1561
  },
1533
- async ({ path, name, type, maxdepth, minsize, maxsize, timeout, ...conn }) => {
1562
+ async ({ path, name, type, maxdepth, minsize, maxsize, newer, timeout, ...conn }) => {
1534
1563
  return connectionPool.withConnection(conn, async (client) => {
1535
- const files = await find(client, { path, name, type, maxdepth, minsize, maxsize }, timeout || 3e4);
1564
+ const files = await find(client, { path, name, type, maxdepth, minsize, maxsize, newer }, timeout || 3e4);
1536
1565
  if (files.length === 0) {
1537
1566
  return { content: [{ type: "text", text: "No files found." }] };
1538
1567
  }
@@ -1547,7 +1576,7 @@ ${files.join("\n")}` }] };
1547
1576
  {
1548
1577
  ...connectionParams,
1549
1578
  path: z.string().describe("Absolute path to the file to tail"),
1550
- lines: z.number().optional().describe("Number of lines to read from the end (default: 100)"),
1579
+ lines: z.number().int().positive().optional().describe("Number of lines to read from the end (default: 100). Must be a positive integer."),
1551
1580
  grep: z.string().optional().describe("Case-insensitive pattern to filter lines"),
1552
1581
  timeout: TimeoutSchema
1553
1582
  },
@@ -1587,7 +1616,7 @@ ${files.join("\n")}` }] };
1587
1616
  if (status.since) lines.push(`Since: ${status.since}`);
1588
1617
  lines.push("");
1589
1618
  lines.push(status.raw);
1590
- return { content: [{ type: "text", text: lines.join("\n") }], isError: !status.active };
1619
+ return { content: [{ type: "text", text: lines.join("\n") }], isError: status.unknown };
1591
1620
  });
1592
1621
  }
1593
1622
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yawlabs/ssh-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "MCP server for SSH operations with built-in diagnostics",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,15 +47,15 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@modelcontextprotocol/sdk": "^1.29.0",
50
- "ssh2": "^1.16.0",
51
- "zod": "^4.3.6"
50
+ "ssh2": "^1.17.0",
51
+ "zod": "^4.4.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@biomejs/biome": "^2.4.13",
55
- "@types/node": "^25.6.0",
56
- "@types/ssh2": "^1.15.4",
54
+ "@biomejs/biome": "^2.4.15",
55
+ "@types/node": "^25.7.0",
56
+ "@types/ssh2": "^1.15.5",
57
57
  "tsup": "^8.5.1",
58
58
  "typescript": "^6.0.3",
59
- "vitest": "^4.1.5"
59
+ "vitest": "^4.1.6"
60
60
  }
61
61
  }