@smartbear/mcp 0.4.0 → 0.6.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.
- package/README.md +15 -121
- package/dist/{insight-hub → bugsnag}/client/api/CurrentUser.js +4 -4
- package/dist/{insight-hub → bugsnag}/client/api/Error.js +37 -4
- package/dist/bugsnag/client/api/Project.js +163 -0
- package/dist/{insight-hub → bugsnag}/client/api/base.js +39 -11
- package/dist/{insight-hub → bugsnag}/client/api/filters.js +2 -2
- package/dist/{insight-hub → bugsnag}/client.js +511 -29
- package/dist/common/info.js +1 -1
- package/dist/common/server.js +17 -5
- package/dist/index.js +11 -11
- package/dist/pactflow/client/ai.js +56 -6
- package/dist/pactflow/client/base.js +19 -1
- package/dist/pactflow/client/prompt-utils.js +89 -0
- package/dist/pactflow/client/prompts.js +133 -0
- package/dist/pactflow/client/tools.js +43 -2
- package/dist/pactflow/client/utils.js +70 -0
- package/dist/pactflow/client.js +192 -13
- package/package.json +9 -4
- package/dist/insight-hub/client/api/Project.js +0 -46
- package/dist/package.json +0 -60
- package/dist/tests/unit/common/server.test.js +0 -319
- package/dist/tests/unit/insight-hub/api-utilities.test.js +0 -31
- package/dist/tests/unit/insight-hub/client.test.js +0 -852
- package/dist/tests/unit/insight-hub/filters.test.js +0 -93
- package/dist/tests/unit/pactflow/ai.test.js +0 -21
- package/dist/tests/unit/pactflow/client.test.js +0 -67
- package/dist/tests/unit/pactflow/tools.test.js +0 -34
- package/dist/vitest.config.js +0 -57
- /package/dist/{insight-hub → bugsnag}/client/api/index.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/configuration.js +0 -0
- /package/dist/{insight-hub → bugsnag}/client/index.js +0 -0
package/dist/pactflow/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { TOOLS } from "./client/tools.js";
|
|
2
1
|
import { MCP_SERVER_NAME, MCP_SERVER_VERSION } from "../common/info.js";
|
|
2
|
+
import { TOOLS } from "./client/tools.js";
|
|
3
|
+
import { getOADMatcherRecommendations, getUserMatcherSelection } from "./client/prompt-utils.js";
|
|
4
|
+
import { PROMPTS } from "./client/prompts.js";
|
|
3
5
|
// Tool definitions for PactFlow AI API client
|
|
4
6
|
export class PactflowClient {
|
|
5
7
|
name = "Contract Testing";
|
|
@@ -8,7 +10,16 @@ export class PactflowClient {
|
|
|
8
10
|
aiBaseUrl;
|
|
9
11
|
baseUrl;
|
|
10
12
|
clientType;
|
|
11
|
-
|
|
13
|
+
server;
|
|
14
|
+
/**
|
|
15
|
+
* Creates an instance of the PactflowClient.
|
|
16
|
+
*
|
|
17
|
+
* @param auth The authentication token or credentials.
|
|
18
|
+
* @param baseUrl The base URL for the API.
|
|
19
|
+
* @param clientType The type of client (e.g., PactFlow, Pact Broker).
|
|
20
|
+
* @param server The SmartBear MCP server instance.
|
|
21
|
+
*/
|
|
22
|
+
constructor(auth, baseUrl, clientType, server) {
|
|
12
23
|
// Set headers based on the type of auth provided
|
|
13
24
|
if (typeof auth === "string") {
|
|
14
25
|
this.headers = {
|
|
@@ -28,34 +39,87 @@ export class PactflowClient {
|
|
|
28
39
|
this.baseUrl = baseUrl;
|
|
29
40
|
this.aiBaseUrl = `${this.baseUrl}/api/ai`;
|
|
30
41
|
this.clientType = clientType;
|
|
42
|
+
this.server = server;
|
|
31
43
|
}
|
|
32
44
|
// PactFlow AI client methods
|
|
33
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Generate new Pact tests based on the provided input.
|
|
47
|
+
*
|
|
48
|
+
* @param toolInput The input data for the generation process.
|
|
49
|
+
* @param getInput Function to get additional input from the user if needed.
|
|
50
|
+
* @returns The result of the generation process.
|
|
51
|
+
* @throws Error if the HTTP request fails or the operation times out.
|
|
52
|
+
*/
|
|
53
|
+
async generate(toolInput, getInput) {
|
|
54
|
+
if (toolInput.openapi?.document && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
|
|
55
|
+
const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
|
|
56
|
+
const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
|
|
57
|
+
toolInput.openapi.matcher = userSelection;
|
|
58
|
+
}
|
|
34
59
|
// Submit the generation request
|
|
35
60
|
const response = await fetch(`${this.aiBaseUrl}/generate`, {
|
|
36
61
|
method: "POST",
|
|
37
62
|
headers: this.headers,
|
|
38
|
-
body: JSON.stringify(
|
|
63
|
+
body: JSON.stringify(toolInput),
|
|
39
64
|
});
|
|
40
65
|
if (!response.ok) {
|
|
41
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
66
|
+
throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
|
|
42
67
|
}
|
|
43
68
|
const status_response = await response.json();
|
|
44
69
|
return await this.pollForCompletion(status_response, "Generation");
|
|
45
70
|
}
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Review the provided Pact tests and suggest improvements.
|
|
73
|
+
*
|
|
74
|
+
* @param toolInput The input data for the review process.
|
|
75
|
+
* @param getInput Function to get additional input from the user if needed.
|
|
76
|
+
* @returns The result of the review process.
|
|
77
|
+
* @throws Error if the HTTP request fails or the operation times out.
|
|
78
|
+
*/
|
|
79
|
+
async review(toolInput, getInput) {
|
|
80
|
+
if (toolInput.openapi?.document && (!toolInput.openapi?.matcher || Object.keys(toolInput.openapi.matcher).length === 0)) {
|
|
81
|
+
const matcherResponse = await getOADMatcherRecommendations(toolInput.openapi.document, this.server);
|
|
82
|
+
const userSelection = await getUserMatcherSelection(matcherResponse, getInput);
|
|
83
|
+
toolInput.openapi.matcher = userSelection;
|
|
84
|
+
}
|
|
85
|
+
// Submit review request
|
|
48
86
|
const response = await fetch(`${this.aiBaseUrl}/review`, {
|
|
49
87
|
method: "POST",
|
|
50
88
|
headers: this.headers,
|
|
51
|
-
body: JSON.stringify(
|
|
89
|
+
body: JSON.stringify(toolInput),
|
|
52
90
|
});
|
|
53
91
|
if (!response.ok) {
|
|
54
|
-
throw new Error(`HTTP error! status: ${response.status}`);
|
|
92
|
+
throw new Error(`HTTP error! status: ${response.status} - ${await response.text()}`);
|
|
55
93
|
}
|
|
56
94
|
const status_response = await response.json();
|
|
57
95
|
return await this.pollForCompletion(status_response, "Review Pacts");
|
|
58
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Retrieves AI status information for the current user
|
|
99
|
+
* and organization.
|
|
100
|
+
*
|
|
101
|
+
* @returns Entitlement containing AI status information, organization
|
|
102
|
+
* entitlements, and user entitlements.
|
|
103
|
+
* @throws Error if the request fails or returns a non-OK response.
|
|
104
|
+
*/
|
|
105
|
+
async getAIStatus() {
|
|
106
|
+
const url = `${this.aiBaseUrl}/entitlement`;
|
|
107
|
+
try {
|
|
108
|
+
const response = await fetch(url, {
|
|
109
|
+
method: "GET",
|
|
110
|
+
headers: this.headers,
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const errorText = await response.text().catch(() => "");
|
|
114
|
+
throw new Error(`PactFlow AI Status Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
|
|
115
|
+
}
|
|
116
|
+
return (await response.json());
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
process.stderr.write(`[GetAICredits] Unexpected error: ${error}\n`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
59
123
|
async getStatus(statusUrl) {
|
|
60
124
|
const response = await fetch(statusUrl, {
|
|
61
125
|
method: "HEAD",
|
|
@@ -108,16 +172,121 @@ export class PactflowClient {
|
|
|
108
172
|
}
|
|
109
173
|
return response.json();
|
|
110
174
|
}
|
|
111
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Checks if a given pacticipant version is safe to deploy
|
|
177
|
+
* to a specified environment.
|
|
178
|
+
*
|
|
179
|
+
* @param body - Input containing:
|
|
180
|
+
* - `pacticipant`: The name of the service (pacticipant).
|
|
181
|
+
* - `version`: The version of the pacticipant being evaluated for deployment.
|
|
182
|
+
* - `environment`: The target environment (e.g., staging, production).
|
|
183
|
+
* @returns CanIDeployResponse containing deployment decision and verification results.
|
|
184
|
+
* @throws Error if the request fails or returns a non-OK response.
|
|
185
|
+
*/
|
|
186
|
+
async canIDeploy(body) {
|
|
187
|
+
const { pacticipant, version, environment } = body;
|
|
188
|
+
const queryParams = new URLSearchParams({
|
|
189
|
+
pacticipant,
|
|
190
|
+
version,
|
|
191
|
+
environment,
|
|
192
|
+
});
|
|
193
|
+
const url = `${this.baseUrl}/can-i-deploy?${queryParams.toString()}`;
|
|
194
|
+
try {
|
|
195
|
+
const response = await fetch(url, {
|
|
196
|
+
method: "GET",
|
|
197
|
+
headers: this.headers,
|
|
198
|
+
});
|
|
199
|
+
if (!response.ok) {
|
|
200
|
+
const errorText = await response.text().catch(() => "");
|
|
201
|
+
throw new Error(`Can-I-Deploy Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
|
|
202
|
+
}
|
|
203
|
+
return (await response.json());
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
console.error(`[CanIDeploy] Unexpected error: ${error}\n`);
|
|
207
|
+
throw error;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Retrieves the matrix of pact verification results for the specified pacticipants.
|
|
212
|
+
* This allows you to see which consumer/provider combinations have been verified
|
|
213
|
+
* and make deployment decisions based on contract test results.
|
|
214
|
+
*
|
|
215
|
+
* @param body - Matrix query parameters including pacticipants, versions, environments, etc.
|
|
216
|
+
* @returns MatrixResponse containing the verification matrix, notices, and summary
|
|
217
|
+
* @throws Error if the request fails or returns a non-OK response
|
|
218
|
+
*/
|
|
219
|
+
async getMatrix(body) {
|
|
220
|
+
const { q, latestby, limit } = body;
|
|
221
|
+
// Build query parameters manually to avoid URL encoding of square brackets
|
|
222
|
+
const queryParts = [];
|
|
223
|
+
// Add optional parameters
|
|
224
|
+
if (latestby) {
|
|
225
|
+
queryParts.push(`latestby=${encodeURIComponent(latestby)}`);
|
|
226
|
+
}
|
|
227
|
+
if (limit !== undefined) {
|
|
228
|
+
queryParts.push(`limit=${limit}`);
|
|
229
|
+
}
|
|
230
|
+
// Add the q parameters (pacticipant selectors)
|
|
231
|
+
q.forEach((selector) => {
|
|
232
|
+
queryParts.push(`q[]pacticipant=${encodeURIComponent(selector.pacticipant)}`);
|
|
233
|
+
if (selector.version) {
|
|
234
|
+
queryParts.push(`q[]version=${encodeURIComponent(selector.version)}`);
|
|
235
|
+
}
|
|
236
|
+
if (selector.branch) {
|
|
237
|
+
queryParts.push(`q[]branch=${encodeURIComponent(selector.branch)}`);
|
|
238
|
+
}
|
|
239
|
+
if (selector.environment) {
|
|
240
|
+
queryParts.push(`q[]environment=${encodeURIComponent(selector.environment)}`);
|
|
241
|
+
}
|
|
242
|
+
if (selector.latest !== undefined) {
|
|
243
|
+
queryParts.push(`q[]latest=${selector.latest}`);
|
|
244
|
+
}
|
|
245
|
+
if (selector.tag) {
|
|
246
|
+
queryParts.push(`q[]tag=${encodeURIComponent(selector.tag)}`);
|
|
247
|
+
}
|
|
248
|
+
if (selector.mainBranch !== undefined) {
|
|
249
|
+
queryParts.push(`q[]mainBranch=${selector.mainBranch}`);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
const url = `${this.baseUrl}/matrix?${queryParts.join('&')}`;
|
|
253
|
+
try {
|
|
254
|
+
const response = await fetch(url, {
|
|
255
|
+
method: "GET",
|
|
256
|
+
headers: this.headers,
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errorText = await response.text().catch(() => "");
|
|
260
|
+
throw new Error(`Matrix Request Failed - status: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`);
|
|
261
|
+
}
|
|
262
|
+
return (await response.json());
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
console.error("[GetMatrix] Unexpected error:", error);
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Registers tools with the provided register function.
|
|
271
|
+
*
|
|
272
|
+
* @param register - The function used to register tools.
|
|
273
|
+
* @param getInput - The function used to get input for tools.
|
|
274
|
+
*/
|
|
275
|
+
registerTools(register, getInput) {
|
|
112
276
|
for (const tool of TOOLS.filter(t => t.clients.includes(this.clientType))) {
|
|
113
|
-
const { handler, clients, formatResponse, ...toolparams } = tool;
|
|
114
|
-
console.log(clients);
|
|
277
|
+
const { handler, clients: _, formatResponse, ...toolparams } = tool; // eslint-disable-line @typescript-eslint/no-unused-vars
|
|
115
278
|
register(toolparams, async (args, _extra) => {
|
|
116
279
|
const handler_fn = this[handler];
|
|
117
280
|
if (typeof handler_fn !== "function") {
|
|
118
281
|
throw new Error(`Handler '${handler}' not found on PactClient`);
|
|
119
282
|
}
|
|
120
|
-
|
|
283
|
+
let result;
|
|
284
|
+
if (tool.enableElicitation) {
|
|
285
|
+
result = await handler_fn.call(this, args, getInput);
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
result = await handler_fn.call(this, args);
|
|
289
|
+
}
|
|
121
290
|
// Use custom response formatter if provided
|
|
122
291
|
if (formatResponse) {
|
|
123
292
|
return formatResponse(result);
|
|
@@ -129,4 +298,14 @@ export class PactflowClient {
|
|
|
129
298
|
});
|
|
130
299
|
}
|
|
131
300
|
}
|
|
301
|
+
/**
|
|
302
|
+
* Registers prompts with the provided register function.
|
|
303
|
+
*
|
|
304
|
+
* @param register - The function used to register prompts.
|
|
305
|
+
*/
|
|
306
|
+
registerPrompts(register) {
|
|
307
|
+
PROMPTS.forEach(prompt => {
|
|
308
|
+
register(prompt.name, prompt.params, prompt.callback);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
132
311
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartbear/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "MCP server for interacting SmartBear Products",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"smartbear",
|
|
7
7
|
"mcp",
|
|
8
|
-
"
|
|
8
|
+
"bugsnag",
|
|
9
9
|
"reflect",
|
|
10
10
|
"api-hub",
|
|
11
11
|
"pactflow"
|
|
12
12
|
],
|
|
13
13
|
"homepage": "https://developer.smartbear.com/smartbear-mcp",
|
|
14
|
+
"mcpName": "com.smartbear/smartbear-mcp",
|
|
14
15
|
"repository": {
|
|
15
16
|
"type": "git",
|
|
16
17
|
"url": "git@github.com:SmartBear/smartbear-mcp.git"
|
|
@@ -44,10 +45,13 @@
|
|
|
44
45
|
"@bugsnag/js": "^8.2.0",
|
|
45
46
|
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
46
47
|
"node-cache": "^5.1.2",
|
|
47
|
-
"
|
|
48
|
+
"swagger-client": "^3.35.6",
|
|
49
|
+
"zod": "^3",
|
|
50
|
+
"zod-to-json-schema": "^3.24.6"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
53
|
"@eslint/js": "^9.29.0",
|
|
54
|
+
"@types/js-yaml": "^4.0.9",
|
|
51
55
|
"@types/node": "^22",
|
|
52
56
|
"@vitest/coverage-v8": "^3.2.4",
|
|
53
57
|
"eslint": "^9.29.0",
|
|
@@ -55,6 +59,7 @@
|
|
|
55
59
|
"shx": "^0.3.4",
|
|
56
60
|
"typescript": "^5.6.2",
|
|
57
61
|
"typescript-eslint": "^8.34.1",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
62
|
+
"vitest": "^3.2.4",
|
|
63
|
+
"vitest-fetch-mock": "^0.4.5"
|
|
59
64
|
}
|
|
60
65
|
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { BaseAPI, pickFieldsFromArray } from "./base.js";
|
|
2
|
-
// --- API Class ---
|
|
3
|
-
export class ProjectAPI extends BaseAPI {
|
|
4
|
-
static eventFieldFields = [
|
|
5
|
-
'custom',
|
|
6
|
-
'display_id',
|
|
7
|
-
'filter_options',
|
|
8
|
-
'pivot_options'
|
|
9
|
-
];
|
|
10
|
-
constructor(configuration) {
|
|
11
|
-
super(configuration);
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* List the Event Fields for a Project
|
|
15
|
-
* GET /projects/{project_id}/event_fields
|
|
16
|
-
* @param projectId The project ID
|
|
17
|
-
* @returns A promise that resolves to the list of event fields
|
|
18
|
-
*/
|
|
19
|
-
async listProjectEventFields(projectId) {
|
|
20
|
-
const url = `/projects/${projectId}/event_fields`;
|
|
21
|
-
const data = await this.request({
|
|
22
|
-
method: 'GET',
|
|
23
|
-
url,
|
|
24
|
-
});
|
|
25
|
-
// Only return allowed fields
|
|
26
|
-
return {
|
|
27
|
-
...data,
|
|
28
|
-
body: pickFieldsFromArray(data.body || [], ProjectAPI.eventFieldFields)
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Create a Project in an Organization
|
|
33
|
-
* POST /organizations/{organization_id}/projects
|
|
34
|
-
* @param organizationId The organization ID
|
|
35
|
-
* @param data The project creation request body
|
|
36
|
-
* @returns A promise that resolves to the created project
|
|
37
|
-
*/
|
|
38
|
-
async createProject(organizationId, data) {
|
|
39
|
-
const url = `/organizations/${organizationId}/projects`;
|
|
40
|
-
return await this.request({
|
|
41
|
-
method: 'POST',
|
|
42
|
-
url,
|
|
43
|
-
body: data,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
}
|
package/dist/package.json
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@smartbear/mcp",
|
|
3
|
-
"version": "0.4.0",
|
|
4
|
-
"description": "MCP server for interacting SmartBear Products",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"smartbear",
|
|
7
|
-
"mcp",
|
|
8
|
-
"insight-hub",
|
|
9
|
-
"reflect",
|
|
10
|
-
"api-hub",
|
|
11
|
-
"pactflow"
|
|
12
|
-
],
|
|
13
|
-
"homepage": "https://developer.smartbear.com/smartbear-mcp",
|
|
14
|
-
"repository": {
|
|
15
|
-
"type": "git",
|
|
16
|
-
"url": "git@github.com:SmartBear/smartbear-mcp.git"
|
|
17
|
-
},
|
|
18
|
-
"license": "MIT",
|
|
19
|
-
"type": "module",
|
|
20
|
-
"bin": {
|
|
21
|
-
"mcp": "dist/index.js"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"dist",
|
|
25
|
-
"assets",
|
|
26
|
-
"**/README.md"
|
|
27
|
-
],
|
|
28
|
-
"config": {
|
|
29
|
-
"mcpServerName": "SmartBear MCP Server"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"build": "tsc && shx chmod +x dist/*.js",
|
|
33
|
-
"lint": "eslint . --ext .ts",
|
|
34
|
-
"prepare": "npm run build",
|
|
35
|
-
"watch": "tsc --watch",
|
|
36
|
-
"test": "vitest",
|
|
37
|
-
"test:watch": "vitest --watch",
|
|
38
|
-
"test:coverage": "vitest --coverage",
|
|
39
|
-
"test:coverage:ci": "vitest --coverage --reporter=verbose",
|
|
40
|
-
"test:run": "vitest run",
|
|
41
|
-
"coverage:check": "vitest --coverage --reporter=verbose --config vitest.config.coverage.ts"
|
|
42
|
-
},
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@bugsnag/js": "^8.2.0",
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.15.0",
|
|
46
|
-
"node-cache": "^5.1.2",
|
|
47
|
-
"zod": "^3"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^9.29.0",
|
|
51
|
-
"@types/node": "^22",
|
|
52
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
53
|
-
"eslint": "^9.29.0",
|
|
54
|
-
"globals": "^16.2.0",
|
|
55
|
-
"shx": "^0.3.4",
|
|
56
|
-
"typescript": "^5.6.2",
|
|
57
|
-
"typescript-eslint": "^8.34.1",
|
|
58
|
-
"vitest": "^3.2.4"
|
|
59
|
-
}
|
|
60
|
-
}
|