@skyramp/mcp 0.0.1

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.
@@ -0,0 +1,439 @@
1
+ import { spawn, exec } from "child_process";
2
+ import path from "path";
3
+ import fs from "fs";
4
+ import os from "os";
5
+ import { promisify } from "util";
6
+ import { logger } from "./logger.js";
7
+ const execAsync = promisify(exec);
8
+ // Mapping of environment variables
9
+ const shellEnvVariables = {
10
+ http_proxy: "http://localhost:8888",
11
+ HTTPS_PROXY: "http://localhost:8888",
12
+ HTTP_PROXY: "http://localhost:8888",
13
+ https_proxy: "http://localhost:8888",
14
+ no_proxy: "",
15
+ NO_PROXY: "",
16
+ };
17
+ let terminalProcess;
18
+ let terminalPID;
19
+ function getTerminalConfig() {
20
+ const platform = os.platform();
21
+ switch (platform) {
22
+ case "win32":
23
+ return {
24
+ command: "cmd",
25
+ args: ["/c", "start", "cmd", "/k"],
26
+ scriptExtension: ".bat",
27
+ scriptHeader: "@echo off",
28
+ pidCommand: "echo %$",
29
+ };
30
+ case "darwin":
31
+ return {
32
+ command: "open",
33
+ args: ["-a", "Terminal"],
34
+ scriptExtension: ".sh",
35
+ scriptHeader: "#!/bin/bash",
36
+ pidCommand: "echo $$",
37
+ };
38
+ case "linux":
39
+ default:
40
+ // Try common Linux terminals in order of preference
41
+ const linuxTerminals = [
42
+ "gnome-terminal",
43
+ "konsole",
44
+ "xfce4-terminal",
45
+ "xterm",
46
+ "terminator",
47
+ "alacritty",
48
+ ];
49
+ // For now, default to gnome-terminal, but this could be enhanced
50
+ // to detect which terminal is available
51
+ return {
52
+ command: "gnome-terminal",
53
+ args: ["--"],
54
+ scriptExtension: ".sh",
55
+ scriptHeader: "#!/bin/bash",
56
+ pidCommand: "echo $$",
57
+ };
58
+ }
59
+ }
60
+ function createEnvironmentScript(config) {
61
+ const tempDir = os.tmpdir();
62
+ const scriptPath = path.join(tempDir, `skyramp-launch${config.scriptExtension}`);
63
+ const pidFile = path.join(tempDir, "skyramp-terminal.pid");
64
+ const infoMessage = os.platform() === "win32"
65
+ ? "A new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs."
66
+ : "\\033[33mA new terminal has been spawned here by Skyramp to enable trace collection.\\nAsk skyramp agent to stop trace to terminate trace collection and return to where you were.\\nFor more information, visit skyramp.dev/docs.\\033[0m";
67
+ let scriptContent;
68
+ if (os.platform() === "win32") {
69
+ // Windows batch script
70
+ const setStatements = Object.entries(shellEnvVariables)
71
+ .map(([key, value]) => `set ${key}=${value}`)
72
+ .join("\n");
73
+ scriptContent = `${config.scriptHeader}
74
+ ${setStatements}
75
+ echo %^%^% > "${pidFile}"
76
+ echo ${infoMessage}
77
+ cmd /k`;
78
+ }
79
+ else {
80
+ // Unix-like script (macOS/Linux)
81
+ const exportStatements = Object.entries(shellEnvVariables)
82
+ .map(([key, value]) => `export ${key}="${value}"`)
83
+ .join("\n");
84
+ scriptContent = `${config.scriptHeader}
85
+ ${exportStatements}
86
+ ${config.pidCommand} > "${pidFile}"
87
+ echo -e "${infoMessage}"
88
+ exec $SHELL`;
89
+ }
90
+ try {
91
+ // Remove existing script if it exists
92
+ if (fs.existsSync(scriptPath)) {
93
+ try {
94
+ fs.unlinkSync(scriptPath);
95
+ logger.debug("Removed existing script file", { scriptPath });
96
+ }
97
+ catch (removeError) {
98
+ logger.warning("Could not remove existing script file", {
99
+ scriptPath,
100
+ error: removeError instanceof Error
101
+ ? removeError.message
102
+ : String(removeError),
103
+ });
104
+ }
105
+ }
106
+ // Create the script
107
+ fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
108
+ // Verify the script was created successfully
109
+ if (!fs.existsSync(scriptPath)) {
110
+ throw new Error("Script file was not created successfully");
111
+ }
112
+ // Check if the script is readable and executable
113
+ try {
114
+ fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
115
+ }
116
+ catch (accessError) {
117
+ logger.warning("Script may not have correct permissions", {
118
+ scriptPath,
119
+ error: accessError instanceof Error
120
+ ? accessError.message
121
+ : String(accessError),
122
+ });
123
+ }
124
+ logger.debug("Cross-platform script created and verified", {
125
+ platform: os.platform(),
126
+ scriptPath,
127
+ scriptSize: fs.statSync(scriptPath).size,
128
+ });
129
+ return scriptPath;
130
+ }
131
+ catch (error) {
132
+ logger.error("Failed to create terminal script", {
133
+ platform: os.platform(),
134
+ scriptPath,
135
+ tempDir,
136
+ error: error instanceof Error ? error.message : String(error),
137
+ });
138
+ throw error;
139
+ }
140
+ }
141
+ async function findAvailableTerminal() {
142
+ if (os.platform() !== "linux") {
143
+ return null; // Use default for non-Linux platforms
144
+ }
145
+ const terminals = [
146
+ "gnome-terminal",
147
+ "konsole",
148
+ "xfce4-terminal",
149
+ "xterm",
150
+ "terminator",
151
+ "alacritty",
152
+ "kitty",
153
+ ];
154
+ for (const terminal of terminals) {
155
+ try {
156
+ await execAsync(`which ${terminal}`);
157
+ logger.debug("Found available terminal", { terminal });
158
+ return terminal;
159
+ }
160
+ catch {
161
+ // Terminal not found, try next
162
+ }
163
+ }
164
+ logger.warning("No common Linux terminal found, falling back to xterm");
165
+ return "xterm";
166
+ }
167
+ export default async function openProxyTerminal() {
168
+ logger.info("Starting cross-platform proxy terminal creation", {
169
+ platform: os.platform(),
170
+ arch: os.arch(),
171
+ });
172
+ try {
173
+ const config = getTerminalConfig();
174
+ // For Linux, try to find an available terminal
175
+ if (os.platform() === "linux") {
176
+ const availableTerminal = await findAvailableTerminal();
177
+ if (availableTerminal && availableTerminal !== "gnome-terminal") {
178
+ config.command = availableTerminal;
179
+ // Adjust args based on terminal
180
+ if (availableTerminal === "xterm") {
181
+ config.args = ["-e"];
182
+ }
183
+ else if (availableTerminal === "konsole") {
184
+ config.args = ["-e"];
185
+ }
186
+ }
187
+ }
188
+ const scriptPath = createEnvironmentScript(config);
189
+ const pidFile = path.join(os.tmpdir(), "skyramp-terminal.pid");
190
+ // Verify script exists before launching terminal
191
+ if (!fs.existsSync(scriptPath)) {
192
+ throw new Error(`Script file does not exist: ${scriptPath}`);
193
+ }
194
+ // Verify script is accessible
195
+ try {
196
+ fs.accessSync(scriptPath, fs.constants.R_OK | fs.constants.X_OK);
197
+ }
198
+ catch (accessError) {
199
+ throw new Error(`Script file is not accessible: ${scriptPath} - ${accessError instanceof Error
200
+ ? accessError.message
201
+ : String(accessError)}`);
202
+ }
203
+ logger.debug("Script verified before terminal launch", {
204
+ scriptPath,
205
+ scriptSize: fs.statSync(scriptPath).size,
206
+ });
207
+ // Launch terminal with script
208
+ const spawnArgs = [...config.args, scriptPath];
209
+ terminalProcess = spawn(config.command, spawnArgs, {
210
+ env: {
211
+ ...process.env,
212
+ ...shellEnvVariables,
213
+ },
214
+ detached: true,
215
+ stdio: "ignore",
216
+ });
217
+ logger.debug("Spawned cross-platform terminal", {
218
+ platform: os.platform(),
219
+ command: config.command,
220
+ args: spawnArgs,
221
+ pid: terminalProcess.pid,
222
+ });
223
+ // Handle process events
224
+ terminalProcess.on("exit", (code) => {
225
+ logger.info("Terminal launch command completed", {
226
+ platform: os.platform(),
227
+ exitCode: code,
228
+ });
229
+ // Wait for terminal to write PID and execute script
230
+ // On macOS, Terminal.app takes longer to start and execute the script
231
+ const cleanupDelay = os.platform() === "darwin" ? 10000 : 3000;
232
+ setTimeout(() => {
233
+ try {
234
+ if (fs.existsSync(pidFile)) {
235
+ const pidContent = fs.readFileSync(pidFile, "utf8").trim();
236
+ const pid = parseInt(pidContent);
237
+ if (!isNaN(pid)) {
238
+ terminalPID = pid;
239
+ logger.info("Terminal PID captured", { terminalPID: pid });
240
+ }
241
+ // Cleanup PID file
242
+ try {
243
+ fs.unlinkSync(pidFile);
244
+ }
245
+ catch (cleanupError) {
246
+ logger.debug("PID file cleanup completed with minor issues", {
247
+ error: cleanupError instanceof Error
248
+ ? cleanupError.message
249
+ : String(cleanupError),
250
+ });
251
+ }
252
+ }
253
+ // Cleanup script file with additional delay and verification
254
+ setTimeout(() => {
255
+ try {
256
+ if (fs.existsSync(scriptPath)) {
257
+ fs.unlinkSync(scriptPath);
258
+ logger.debug("Script file cleaned up", { scriptPath });
259
+ }
260
+ }
261
+ catch (scriptCleanupError) {
262
+ logger.debug("Script cleanup completed with minor issues", {
263
+ error: scriptCleanupError instanceof Error
264
+ ? scriptCleanupError.message
265
+ : String(scriptCleanupError),
266
+ });
267
+ }
268
+ }, 2000);
269
+ }
270
+ catch (err) {
271
+ logger.warning("Could not read terminal PID", {
272
+ error: err instanceof Error ? err.message : String(err),
273
+ });
274
+ // Even if PID reading fails, still cleanup script after delay
275
+ setTimeout(() => {
276
+ try {
277
+ if (fs.existsSync(scriptPath)) {
278
+ fs.unlinkSync(scriptPath);
279
+ logger.debug("Script file cleaned up (fallback)", {
280
+ scriptPath,
281
+ });
282
+ }
283
+ }
284
+ catch (scriptCleanupError) {
285
+ logger.debug("Fallback script cleanup completed with minor issues", {
286
+ error: scriptCleanupError instanceof Error
287
+ ? scriptCleanupError.message
288
+ : String(scriptCleanupError),
289
+ });
290
+ }
291
+ }, 2000);
292
+ }
293
+ }, cleanupDelay);
294
+ });
295
+ terminalProcess.on("error", (error) => {
296
+ logger.error("Terminal spawn error", {
297
+ platform: os.platform(),
298
+ command: config.command,
299
+ error: error.message,
300
+ });
301
+ });
302
+ logger.info("Cross-platform proxy terminal opened successfully", {
303
+ platform: os.platform(),
304
+ arch: os.arch(),
305
+ });
306
+ }
307
+ catch (error) {
308
+ logger.error("Failed to open proxy terminal", {
309
+ platform: os.platform(),
310
+ error: error instanceof Error ? error.message : String(error),
311
+ });
312
+ throw error;
313
+ }
314
+ }
315
+ export async function closeProxyTerminal() {
316
+ logger.info("Starting cross-platform proxy terminal closure", {
317
+ platform: os.platform(),
318
+ });
319
+ // Kill the spawn process if it exists
320
+ if (terminalProcess) {
321
+ const killResult = terminalProcess.kill();
322
+ logger.debug("Terminal spawn process kill result", { killResult });
323
+ }
324
+ // Handle platform-specific terminal closing
325
+ if (terminalPID) {
326
+ try {
327
+ const platform = os.platform();
328
+ if (platform === "darwin") {
329
+ // macOS: First try to kill the specific PID and its process group
330
+ try {
331
+ // Kill the process group to ensure child processes are also terminated
332
+ process.kill(-terminalPID, "SIGTERM");
333
+ logger.info("macOS terminal process group killed via SIGTERM", {
334
+ terminalPID,
335
+ });
336
+ // Give processes time to terminate gracefully
337
+ await new Promise((resolve) => setTimeout(resolve, 1000));
338
+ // Check if process still exists and force kill if needed
339
+ try {
340
+ process.kill(terminalPID, 0); // Check if process exists
341
+ process.kill(-terminalPID, "SIGKILL");
342
+ logger.info("macOS terminal process group force killed", {
343
+ terminalPID,
344
+ });
345
+ }
346
+ catch (killErr) {
347
+ // Process is already dead, which is what we want
348
+ logger.debug("Terminal process already terminated", {
349
+ terminalPID,
350
+ });
351
+ }
352
+ }
353
+ catch (pidErr) {
354
+ logger.debug("PID-based kill failed, trying AppleScript fallback", {
355
+ error: pidErr instanceof Error ? pidErr.message : String(pidErr),
356
+ });
357
+ // Fallback: Use AppleScript to close terminal windows with our proxy environment
358
+ const closeScript = `tell application "Terminal"
359
+ repeat with w in windows
360
+ repeat with t in tabs of w
361
+ try
362
+ -- Send exit command to close the shell in this tab
363
+ do script "exit" in t
364
+ end try
365
+ end repeat
366
+ end repeat
367
+ end tell`;
368
+ await execAsync(`osascript -e '${closeScript.replace(/'/g, "'\\''")}'`);
369
+ logger.info("macOS terminal closed via AppleScript fallback");
370
+ }
371
+ }
372
+ else if (platform === "win32") {
373
+ // Windows: Use taskkill with /T flag to kill process tree
374
+ await execAsync(`taskkill /PID ${terminalPID} /T /F`);
375
+ logger.info("Windows terminal and children closed via taskkill", {
376
+ terminalPID,
377
+ });
378
+ }
379
+ else {
380
+ // Linux: Send SIGTERM to process group
381
+ try {
382
+ process.kill(-terminalPID, "SIGTERM");
383
+ logger.info("Linux terminal process group closed via SIGTERM", {
384
+ terminalPID,
385
+ });
386
+ // Give processes time to terminate gracefully
387
+ await new Promise((resolve) => setTimeout(resolve, 1000));
388
+ // Check if process still exists and force kill if needed
389
+ try {
390
+ process.kill(terminalPID, 0); // Check if process exists
391
+ process.kill(-terminalPID, "SIGKILL");
392
+ logger.info("Linux terminal process group force killed", {
393
+ terminalPID,
394
+ });
395
+ }
396
+ catch (killErr) {
397
+ // Process is already dead, which is what we want
398
+ logger.debug("Terminal process already terminated", {
399
+ terminalPID,
400
+ });
401
+ }
402
+ }
403
+ catch (pidErr) {
404
+ // Fallback to regular process kill
405
+ try {
406
+ process.kill(terminalPID, "SIGTERM");
407
+ logger.info("Linux terminal closed via fallback SIGTERM", {
408
+ terminalPID,
409
+ });
410
+ }
411
+ catch (fallbackErr) {
412
+ logger.warning("Could not kill terminal process", {
413
+ terminalPID,
414
+ error: fallbackErr instanceof Error
415
+ ? fallbackErr.message
416
+ : String(fallbackErr),
417
+ });
418
+ }
419
+ }
420
+ }
421
+ }
422
+ catch (err) {
423
+ logger.warning("Error during platform-specific terminal closure", {
424
+ platform: os.platform(),
425
+ terminalPID,
426
+ error: err instanceof Error ? err.message : String(err),
427
+ });
428
+ }
429
+ }
430
+ else {
431
+ logger.warning("No terminal PID available for cleanup - terminal may not have been properly tracked");
432
+ }
433
+ // Reset state
434
+ terminalProcess = undefined;
435
+ terminalPID = undefined;
436
+ logger.info("Cross-platform proxy terminal closure completed", {
437
+ platform: os.platform(),
438
+ });
439
+ }
@@ -0,0 +1,118 @@
1
+ import path from "path";
2
+ import { logger } from "./logger.js";
3
+ export const OUTPUT_DIR_FIELD_NAME = "outputDir";
4
+ export const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
5
+ export const PLAYWRIGHT_OUTPUT_FILE_FIELD_NAME = "playwrightOutput";
6
+ export const PATH_PARAMS_FIELD_NAME = "pathParams";
7
+ export const QUERY_PARAMS_FIELD_NAME = "queryParams";
8
+ export const FORM_PARAMS_FIELD_NAME = "formParams";
9
+ export const TELEMETRY_entrypoint_FIELD_NAME = "mcp";
10
+ export function IsValidKeyValueList(input) {
11
+ // Regular expression to match a comma-separated list of key:value pairs
12
+ const regex = /^([a-zA-Z0-9_-]+)(,([a-zA-Z0-9_-]+))*$/;
13
+ return regex.test(input);
14
+ }
15
+ export function getPathParameterValidationError(requiredPathParams) {
16
+ if (requiredPathParams.split(",").length > 1) {
17
+ return (`Please specify values for the following required path parameters: \n` +
18
+ `${requiredPathParams
19
+ .split(",")
20
+ .map((param, index) => `${index + 1}. \`${param.trim()}\``)
21
+ .join("\n")}\n\n`);
22
+ }
23
+ else {
24
+ return (`Please specify value for the following required path parameter:\n` +
25
+ `${requiredPathParams
26
+ .split(",")
27
+ .map((param, index) => `- \`${param.trim()}\``)
28
+ .join("\n")}\n\n`);
29
+ }
30
+ }
31
+ export function validatePath(dir, fieldName) {
32
+ if (dir !== "" && !path.isAbsolute(dir)) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Error: Relative path provided for ${fieldName}: "${dir}"
38
+
39
+ **Root Cause:** This tool requires absolute paths, but a relative path was provided.
40
+
41
+ **Solution:** Use the appropriate Skyramp prompt first to resolve relative paths to absolute paths:
42
+ - For trace collection: Use "skyramp_stop_trace_collection_prompt"
43
+ - For test generation: Provide absolute paths directly
44
+
45
+ **How prompts work:**
46
+ 1. Prompts take your relative paths + current working directory from your IDE
47
+ 2. They resolve relative paths to absolute paths
48
+ 3. They provide the exact absolute paths to use with tools
49
+
50
+ **Alternative:** Provide absolute paths directly (e.g., "/Users/username/project/output.json")
51
+
52
+ Current working directory context is required when using relative paths.`,
53
+ },
54
+ ],
55
+ isError: true,
56
+ };
57
+ }
58
+ return null;
59
+ }
60
+ export function validateParams(params, fieldName) {
61
+ const trimmedParams = params.trim();
62
+ // Reject JSON-like input
63
+ if (trimmedParams.startsWith("{") && trimmedParams.endsWith("}")) {
64
+ return {
65
+ content: [
66
+ {
67
+ type: "text",
68
+ text: `Error: ${fieldName} must be a comma-separated string in the format key=value or key1=value1,key2=value2 — not a JSON object.`,
69
+ },
70
+ ],
71
+ isError: true,
72
+ };
73
+ }
74
+ // Allow empty input
75
+ if (!trimmedParams)
76
+ return null;
77
+ // Validate comma-separated key=value pairs
78
+ const pairs = trimmedParams.split(",");
79
+ for (const pair of pairs) {
80
+ const [key, value] = pair.split("=");
81
+ if (!key ||
82
+ value === undefined ||
83
+ key.trim() === "" ||
84
+ value.trim() === "") {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `Error: Each entry in ${fieldName} must be in the format key=value. Invalid entry: "${pair}".`,
90
+ },
91
+ ],
92
+ isError: true,
93
+ };
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ // check if the value is a path and if it a relative throw an error else return path appended with `@`
99
+ export function validateRequestData(value) {
100
+ if (value.includes("@")) {
101
+ return value;
102
+ }
103
+ //if value is valid json string return value
104
+ try {
105
+ JSON.parse(value);
106
+ return value;
107
+ }
108
+ catch (e) {
109
+ logger.warning("Request data is not a valid JSON string", { value });
110
+ }
111
+ const isAbsolute = path.isAbsolute(value);
112
+ logger.debug("Path validation check", { value, isAbsolute });
113
+ //if value is path
114
+ if (isAbsolute) {
115
+ return "@" + value;
116
+ }
117
+ return null;
118
+ }
@@ -0,0 +1,32 @@
1
+ // @ts-ignore
2
+ import { validateParams } from "./utils.js";
3
+ describe("validateParams", () => {
4
+ it("should return null for valid comma-separated key=value pairs", () => {
5
+ const result = validateParams("foo=bar,baz=qux", "testField");
6
+ expect(result).toBeNull();
7
+ });
8
+ it("should return null for empty string", () => {
9
+ const result = validateParams("", "testField");
10
+ expect(result).toBeNull();
11
+ });
12
+ it("should return error for JSON-like input", () => {
13
+ const result = validateParams('{"foo":"bar"}', "testField");
14
+ expect(result?.isError).toBe(true);
15
+ expect(result?.content?.[0].text).toMatch(/not a JSON object/);
16
+ });
17
+ it("should return error for missing value", () => {
18
+ const result = validateParams("foo=", "testField");
19
+ expect(result?.isError).toBe(true);
20
+ expect(result?.content?.[0].text).toMatch(/key=value/);
21
+ });
22
+ it("should return error for missing key", () => {
23
+ const result = validateParams("=bar", "testField");
24
+ expect(result?.isError).toBe(true);
25
+ expect(result?.content?.[0].text).toMatch(/key=value/);
26
+ });
27
+ it("should return error for missing key and value", () => {
28
+ const result = validateParams("=", "testField");
29
+ expect(result?.isError).toBe(true);
30
+ expect(result?.content?.[0].text).toMatch(/key=value/);
31
+ });
32
+ });
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@skyramp/mcp",
3
+ "version": "0.0.1",
4
+ "main": "build/index.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "mcp": "./build/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc && chmod 755 build/index.js",
11
+ "build:prod": "tsc --sourceMap false && chmod 755 build/index.js",
12
+ "pack": "npm run build:prod && npm pack",
13
+ "start": "node build/index.js",
14
+ "test:startup": "time node build/index.js --help",
15
+ "pretty": "npx prettier --write .",
16
+ "test": "npx jest"
17
+ },
18
+ "files": [
19
+ "build/**/*.js",
20
+ "README.md"
21
+ ],
22
+ "author": "Skyramp Team",
23
+ "license": "ISC",
24
+ "description": "Skyramp MCP (Model Context Protocol) Server - AI-powered test generation and execution",
25
+ "keywords": [
26
+ "mcp",
27
+ "model-context-protocol",
28
+ "skyramp",
29
+ "testing",
30
+ "test-generation",
31
+ "ai-testing",
32
+ "cursor",
33
+ "claude"
34
+ ],
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/skyramp/mcp.git"
38
+ },
39
+ "homepage": "https://github.com/skyramp/mcp#readme",
40
+ "bugs": {
41
+ "url": "https://github.com/skyramp/mcp/issues"
42
+ },
43
+ "dependencies": {
44
+ "@modelcontextprotocol/sdk": "^1.11.4",
45
+ "@skyramp/skyramp": "^1.2.7",
46
+ "dockerode": "^4.0.6",
47
+ "zod": "^3.25.3"
48
+ },
49
+ "devDependencies": {
50
+ "@types/dockerode": "^3.3.39",
51
+ "@types/jest": "^29.5.14",
52
+ "@types/mocha": "^10.0.10",
53
+ "@types/node": "^22.15.19",
54
+ "jest": "^29.7.0",
55
+ "ts-jest": "^29.3.4",
56
+ "typescript": "^5.8.3"
57
+ },
58
+ "engines": {
59
+ "node": ">=18"
60
+ }
61
+ }