@stubbedev/trimit-mcp 0.1.0 → 0.1.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/README.md +156 -138
- package/dist/categories/customers.js +3 -3
- package/dist/categories/salesdocs.js +19 -16
- package/dist/server.js +379 -2
- package/dist/spec/entities.js +406 -0
- package/dist/spec/enums.js +55 -0
- package/dist/spec/lint.js +111 -0
- package/dist/spec/odata.js +294 -0
- package/dist/spec/payloads.js +89 -0
- package/dist/spec/types.js +1 -0
- package/dist/spec/validate.js +368 -0
- package/package.json +9 -8
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { ENTITIES, findEntityByResourcePath, getEntity } from "./entities.js";
|
|
2
|
+
import { ENUMS } from "./enums.js";
|
|
3
|
+
import { checkOdata } from "./odata.js";
|
|
4
|
+
function parseEndpoint(endpoint) {
|
|
5
|
+
const [pathPart, queryPart = ""] = endpoint.split("?");
|
|
6
|
+
const query = {};
|
|
7
|
+
for (const pair of queryPart.split("&").filter(Boolean)) {
|
|
8
|
+
const idx = pair.indexOf("=");
|
|
9
|
+
const k = idx >= 0 ? pair.slice(0, idx) : pair;
|
|
10
|
+
const v = idx >= 0 ? decodeURIComponent(pair.slice(idx + 1)) : "";
|
|
11
|
+
query[k] = v;
|
|
12
|
+
}
|
|
13
|
+
let apiBase = "unknown";
|
|
14
|
+
if (/\/api\/trimit\/integration\/v1\.\d+\//.test(pathPart))
|
|
15
|
+
apiBase = "trimit";
|
|
16
|
+
else if (/\/api\/v2\.0\//.test(pathPart))
|
|
17
|
+
apiBase = "std";
|
|
18
|
+
else if (/\/ODataV4(\/|$)/.test(pathPart))
|
|
19
|
+
apiBase = "odata";
|
|
20
|
+
// Resource segment after companies({id})/ or after the apiBase path
|
|
21
|
+
const compMatch = pathPart.match(/companies\(([^)]*)\)\/([^/(?]+)(\(([^)]+)\))?/);
|
|
22
|
+
const altMatch = pathPart.match(/\/(?:api\/v2\.0|api\/trimit\/integration\/v1\.\d+|ODataV4)\/([^/(?]+)(\(([^)]+)\))?$/);
|
|
23
|
+
const resource = compMatch?.[2] ?? altMatch?.[1];
|
|
24
|
+
const keyExpr = compMatch?.[4] ?? altMatch?.[3];
|
|
25
|
+
return { apiBase, resource, keyExpr, query, rawQuery: queryPart };
|
|
26
|
+
}
|
|
27
|
+
function headerLookup(headers, name) {
|
|
28
|
+
if (!headers)
|
|
29
|
+
return undefined;
|
|
30
|
+
const key = Object.keys(headers).find((k) => k.toLowerCase() === name.toLowerCase());
|
|
31
|
+
return key ? headers[key] : undefined;
|
|
32
|
+
}
|
|
33
|
+
function bodyHasDecimalField(body, entity) {
|
|
34
|
+
if (!entity || !body || typeof body !== "object")
|
|
35
|
+
return [];
|
|
36
|
+
const obj = body;
|
|
37
|
+
const out = [];
|
|
38
|
+
for (const f of entity.fields) {
|
|
39
|
+
if (!f.decimal)
|
|
40
|
+
continue;
|
|
41
|
+
if (f.name in obj)
|
|
42
|
+
out.push(f.name);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function validateBodyAgainstEntity(body, entity, mode, pathPrefix = "") {
|
|
47
|
+
const issues = [];
|
|
48
|
+
const fieldMap = new Map();
|
|
49
|
+
for (const f of entity.fields)
|
|
50
|
+
fieldMap.set(f.name, f);
|
|
51
|
+
for (const nav of entity.navigationProperties) {
|
|
52
|
+
if (!fieldMap.has(nav.name)) {
|
|
53
|
+
fieldMap.set(nav.name, { name: nav.name, type: "array", itemEntity: nav.target });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (mode === "create") {
|
|
57
|
+
for (const f of entity.fields) {
|
|
58
|
+
if (f.required && !(f.name in body)) {
|
|
59
|
+
issues.push({
|
|
60
|
+
severity: "error",
|
|
61
|
+
code: "body.missing-required",
|
|
62
|
+
path: `${pathPrefix}${f.name}`,
|
|
63
|
+
message: `Field '${f.name}' is required on ${entity.name} create.`,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const [key, value] of Object.entries(body)) {
|
|
69
|
+
const field = fieldMap.get(key);
|
|
70
|
+
if (!field) {
|
|
71
|
+
issues.push({
|
|
72
|
+
severity: "warning",
|
|
73
|
+
code: "body.unknown-field",
|
|
74
|
+
path: `${pathPrefix}${key}`,
|
|
75
|
+
message: `Field '${key}' is not declared on entity '${entity.name}'. Possible typo or undocumented configuredField — verify against $metadata.`,
|
|
76
|
+
});
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (field.readOnly) {
|
|
80
|
+
issues.push({
|
|
81
|
+
severity: "warning",
|
|
82
|
+
code: "body.readonly-write",
|
|
83
|
+
path: `${pathPrefix}${key}`,
|
|
84
|
+
message: `Field '${key}' is server-set (readOnly). Sending it will likely be ignored or rejected.`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (mode === "patch" && field.mutable === false) {
|
|
88
|
+
issues.push({
|
|
89
|
+
severity: "error",
|
|
90
|
+
code: "body.immutable",
|
|
91
|
+
path: `${pathPrefix}${key}`,
|
|
92
|
+
message: `Field '${key}' is not mutable — cannot PATCH.`,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
issues.push(...checkFieldValue(field, value, `${pathPrefix}${key}`));
|
|
96
|
+
}
|
|
97
|
+
return issues;
|
|
98
|
+
}
|
|
99
|
+
function checkFieldValue(field, value, path) {
|
|
100
|
+
const issues = [];
|
|
101
|
+
if (value === null || value === undefined)
|
|
102
|
+
return issues;
|
|
103
|
+
if (field.enumRef) {
|
|
104
|
+
const e = ENUMS[field.enumRef];
|
|
105
|
+
if (e && typeof value === "string") {
|
|
106
|
+
if (e.caseSensitive) {
|
|
107
|
+
if (!e.values.includes(value)) {
|
|
108
|
+
const ciHit = e.values.find((v) => v.toLowerCase() === value.toLowerCase());
|
|
109
|
+
issues.push({
|
|
110
|
+
severity: "error",
|
|
111
|
+
code: "body.bad-enum",
|
|
112
|
+
path,
|
|
113
|
+
message: `Value '${value}' not in enum ${field.enumRef}. Allowed: ${e.values.map((v) => `'${v}'`).join(", ")}.`,
|
|
114
|
+
fix: ciHit ? `Use exact casing: '${ciHit}'.` : undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
else if (!e.values.map((v) => v.toLowerCase()).includes(value.toLowerCase())) {
|
|
119
|
+
issues.push({
|
|
120
|
+
severity: "error",
|
|
121
|
+
code: "body.bad-enum",
|
|
122
|
+
path,
|
|
123
|
+
message: `Value '${value}' not in enum ${field.enumRef}.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return issues;
|
|
128
|
+
}
|
|
129
|
+
switch (field.type) {
|
|
130
|
+
case "string":
|
|
131
|
+
if (typeof value !== "string") {
|
|
132
|
+
issues.push({ severity: "error", code: "body.type", path, message: `Expected string, got ${typeof value}.` });
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case "integer":
|
|
136
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
137
|
+
issues.push({ severity: "error", code: "body.type", path, message: `Expected integer.` });
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
case "number":
|
|
141
|
+
if (typeof value !== "number") {
|
|
142
|
+
issues.push({ severity: "error", code: "body.type", path, message: `Expected number.` });
|
|
143
|
+
}
|
|
144
|
+
break;
|
|
145
|
+
case "decimal":
|
|
146
|
+
if (typeof value !== "number" && typeof value !== "string") {
|
|
147
|
+
issues.push({
|
|
148
|
+
severity: "error",
|
|
149
|
+
code: "body.type",
|
|
150
|
+
path,
|
|
151
|
+
message: `Decimal must be number or string (when IEEE754Compatible:true).`,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
case "boolean":
|
|
156
|
+
if (typeof value !== "boolean") {
|
|
157
|
+
issues.push({ severity: "error", code: "body.type", path, message: `Expected boolean.` });
|
|
158
|
+
}
|
|
159
|
+
break;
|
|
160
|
+
case "date":
|
|
161
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
162
|
+
issues.push({
|
|
163
|
+
severity: "error",
|
|
164
|
+
code: "body.bad-date",
|
|
165
|
+
path,
|
|
166
|
+
message: `Expected date YYYY-MM-DD, got '${String(value)}'.`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
case "datetime":
|
|
171
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
172
|
+
issues.push({
|
|
173
|
+
severity: "warning",
|
|
174
|
+
code: "body.bad-datetime",
|
|
175
|
+
path,
|
|
176
|
+
message: `Expected ISO 8601 datetime with 'T'.`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
break;
|
|
180
|
+
case "guid":
|
|
181
|
+
if (typeof value !== "string" || !/^[0-9a-fA-F-]{36}$/.test(value)) {
|
|
182
|
+
issues.push({ severity: "warning", code: "body.bad-guid", path, message: `Expected GUID.` });
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
case "array":
|
|
186
|
+
if (!Array.isArray(value)) {
|
|
187
|
+
issues.push({ severity: "error", code: "body.type", path, message: `Expected array.` });
|
|
188
|
+
}
|
|
189
|
+
else if (field.itemEntity) {
|
|
190
|
+
const childEntity = getEntity(field.itemEntity);
|
|
191
|
+
if (childEntity) {
|
|
192
|
+
value.forEach((item, idx) => {
|
|
193
|
+
if (item && typeof item === "object") {
|
|
194
|
+
issues.push(...validateBodyAgainstEntity(item, childEntity, "create", `${path}[${idx}].`));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
return issues;
|
|
202
|
+
}
|
|
203
|
+
export function validateRequest(input) {
|
|
204
|
+
const issues = [];
|
|
205
|
+
const parsed = parseEndpoint(input.endpoint);
|
|
206
|
+
if (parsed.apiBase === "unknown") {
|
|
207
|
+
issues.push({
|
|
208
|
+
severity: "warning",
|
|
209
|
+
code: "endpoint.base",
|
|
210
|
+
message: "Endpoint does not match a known TRIMIT / BC base. Expected /api/trimit/integration/v1.x, /api/v2.0, or /ODataV4.",
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Placeholder substitution
|
|
214
|
+
for (const placeholder of ["{tenant}", "{environment}", "{companyId}", "{token}"]) {
|
|
215
|
+
if (placeholder !== "{token}" && input.endpoint.includes(placeholder)) {
|
|
216
|
+
issues.push({
|
|
217
|
+
severity: "info",
|
|
218
|
+
code: "endpoint.placeholder",
|
|
219
|
+
message: `Endpoint still contains ${placeholder} — substitute before issuing the request.`,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (headerLookup(input.headers, "Authorization")?.includes("{token}")) {
|
|
224
|
+
issues.push({
|
|
225
|
+
severity: "info",
|
|
226
|
+
code: "headers.token-placeholder",
|
|
227
|
+
message: "Authorization header still contains {token} placeholder.",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// Method-specific header checks
|
|
231
|
+
const ifMatch = headerLookup(input.headers, "If-Match");
|
|
232
|
+
const ieee = headerLookup(input.headers, "IEEE754Compatible");
|
|
233
|
+
const contentType = headerLookup(input.headers, "Content-Type");
|
|
234
|
+
if (input.method === "PATCH" || input.method === "DELETE") {
|
|
235
|
+
if (!ifMatch) {
|
|
236
|
+
issues.push({
|
|
237
|
+
severity: "error",
|
|
238
|
+
code: "headers.missing-if-match",
|
|
239
|
+
message: `BC OData requires If-Match on ${input.method}.`,
|
|
240
|
+
fix: "Add header `If-Match: *` (unconditional) or pass the @odata.etag value from a prior GET.",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if ((input.method === "POST" || input.method === "PATCH" || input.method === "PUT") && input.body !== undefined) {
|
|
245
|
+
if (!contentType) {
|
|
246
|
+
issues.push({
|
|
247
|
+
severity: "error",
|
|
248
|
+
code: "headers.missing-content-type",
|
|
249
|
+
message: "Body present without Content-Type header.",
|
|
250
|
+
fix: "Add `Content-Type: application/json`.",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
else if (!/application\/json/i.test(contentType)) {
|
|
254
|
+
issues.push({
|
|
255
|
+
severity: "warning",
|
|
256
|
+
code: "headers.unexpected-content-type",
|
|
257
|
+
message: `Content-Type is '${contentType}'. BC OData expects application/json.`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (!headerLookup(input.headers, "Authorization")) {
|
|
262
|
+
issues.push({
|
|
263
|
+
severity: "error",
|
|
264
|
+
code: "headers.missing-auth",
|
|
265
|
+
message: "Missing Authorization header. Send `Authorization: Bearer {token}`.",
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
// Resolve entity
|
|
269
|
+
const entity = parsed.resource ? findEntityByResourcePath(parsed.resource) : undefined;
|
|
270
|
+
// Check $batch
|
|
271
|
+
if (parsed.resource === "$batch") {
|
|
272
|
+
if (input.method !== "POST") {
|
|
273
|
+
issues.push({
|
|
274
|
+
severity: "error",
|
|
275
|
+
code: "batch.method",
|
|
276
|
+
message: "$batch requires POST.",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (input.body && typeof input.body === "object") {
|
|
280
|
+
const requests = input.body.requests;
|
|
281
|
+
if (!Array.isArray(requests)) {
|
|
282
|
+
issues.push({
|
|
283
|
+
severity: "error",
|
|
284
|
+
code: "batch.no-requests",
|
|
285
|
+
message: "$batch body must contain `requests: []`.",
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
requests.forEach((req, idx) => {
|
|
290
|
+
if (!req || typeof req !== "object")
|
|
291
|
+
return;
|
|
292
|
+
const r = req;
|
|
293
|
+
if (!r.id) {
|
|
294
|
+
issues.push({
|
|
295
|
+
severity: "error",
|
|
296
|
+
code: "batch.missing-id",
|
|
297
|
+
path: `requests[${idx}]`,
|
|
298
|
+
message: "Each sub-request needs a unique 'id'.",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (typeof r.url !== "string") {
|
|
302
|
+
issues.push({
|
|
303
|
+
severity: "error",
|
|
304
|
+
code: "batch.missing-url",
|
|
305
|
+
path: `requests[${idx}]`,
|
|
306
|
+
message: "Each sub-request needs a relative 'url'.",
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
else if (/^https?:/.test(r.url) || r.url.startsWith("/")) {
|
|
310
|
+
issues.push({
|
|
311
|
+
severity: "warning",
|
|
312
|
+
code: "batch.absolute-url",
|
|
313
|
+
path: `requests[${idx}].url`,
|
|
314
|
+
message: "Sub-request URLs should be relative to the batch base, not absolute.",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
if (typeof r.method === "string" && ["POST", "PATCH", "PUT"].includes(r.method) && r.body !== undefined) {
|
|
318
|
+
const subHeaders = r.headers;
|
|
319
|
+
if (!subHeaders || !Object.keys(subHeaders).some((k) => k.toLowerCase() === "content-type")) {
|
|
320
|
+
issues.push({
|
|
321
|
+
severity: "warning",
|
|
322
|
+
code: "batch.sub-missing-content-type",
|
|
323
|
+
path: `requests[${idx}].headers`,
|
|
324
|
+
message: "Sub-request with body should set Content-Type: application/json.",
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Validate body fields against entity schema
|
|
333
|
+
if (entity && input.body && typeof input.body === "object" && !Array.isArray(input.body)) {
|
|
334
|
+
const mode = input.method === "POST" ? "create" : "patch";
|
|
335
|
+
if (input.method === "POST" || input.method === "PATCH") {
|
|
336
|
+
issues.push(...validateBodyAgainstEntity(input.body, entity, mode));
|
|
337
|
+
}
|
|
338
|
+
const decimalFieldsInBody = bodyHasDecimalField(input.body, entity);
|
|
339
|
+
if (decimalFieldsInBody.length && ieee?.toLowerCase() !== "true") {
|
|
340
|
+
issues.push({
|
|
341
|
+
severity: "warning",
|
|
342
|
+
code: "headers.missing-ieee754",
|
|
343
|
+
message: `Body includes decimal fields (${decimalFieldsInBody.join(", ")}) without IEEE754Compatible: true. JS numbers can lose precision on large/high-scale decimals — BC convention is to send/receive these as strings.`,
|
|
344
|
+
fix: "Add header `IEEE754Compatible: true` and send decimals as JSON strings ('123.45').",
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// OData query check
|
|
349
|
+
if (parsed.query["$filter"] || parsed.query["$select"] || parsed.query["$expand"] || parsed.query["$orderby"] || parsed.query["$top"] || parsed.query["$skip"]) {
|
|
350
|
+
const odataIssues = checkOdata({
|
|
351
|
+
entity: entity?.name,
|
|
352
|
+
resourcePath: parsed.resource,
|
|
353
|
+
filter: parsed.query["$filter"],
|
|
354
|
+
select: parsed.query["$select"]?.split(",").map((s) => s.trim()),
|
|
355
|
+
expand: parsed.query["$expand"],
|
|
356
|
+
orderby: parsed.query["$orderby"],
|
|
357
|
+
top: parsed.query["$top"] ? Number(parsed.query["$top"]) : undefined,
|
|
358
|
+
skip: parsed.query["$skip"] ? Number(parsed.query["$skip"]) : undefined,
|
|
359
|
+
});
|
|
360
|
+
for (const i of odataIssues) {
|
|
361
|
+
issues.push({ severity: i.severity, code: i.code, message: i.message });
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return issues;
|
|
365
|
+
}
|
|
366
|
+
export function listEntities() {
|
|
367
|
+
return Object.keys(ENTITIES);
|
|
368
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stubbedev/trimit-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "MCP server for TRIMIT (Microsoft Dynamics 365 Business Central) API development assistance",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,15 +21,16 @@
|
|
|
21
21
|
"smoke": "npm run build && npm run smoke:tools",
|
|
22
22
|
"smoke:tools": "printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | node dist/index.js",
|
|
23
23
|
"smoke:validate": "printf '%s\\n' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/list\",\"params\":{}}' | node dist/index.js | node -e \"let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const r=JSON.parse(d.trim());if(r.error)throw new Error('server error: '+r.error.message);const t=r.result?.tools;if(!Array.isArray(t)||!t.length)throw new Error('no tools in response');const bad=t.filter(x=>!x.name||!x.inputSchema);if(bad.length)throw new Error('malformed tools: '+bad.map(x=>x.name??'(unnamed)').join(', '));console.log('smoke OK — '+t.length+' tool(s): '+t.map(x=>x.name).join(', '))}catch(e){console.error('smoke FAIL:',e.message);process.exit(1)}})\"",
|
|
24
|
+
"preversion": "npm run build && npm run smoke:validate",
|
|
24
25
|
"prerelease": "npm run build && npm run smoke:validate && ncu"
|
|
25
26
|
},
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/stubbedev/trimit-mcp.git"
|
|
29
|
+
"url": "git+https://github.com/stubbedev/trimit-dev-mcp.git"
|
|
29
30
|
},
|
|
30
|
-
"homepage": "https://github.com/stubbedev/trimit-mcp#readme",
|
|
31
|
+
"homepage": "https://github.com/stubbedev/trimit-dev-mcp#readme",
|
|
31
32
|
"bugs": {
|
|
32
|
-
"url": "https://github.com/stubbedev/trimit-mcp/issues"
|
|
33
|
+
"url": "https://github.com/stubbedev/trimit-dev-mcp/issues"
|
|
33
34
|
},
|
|
34
35
|
"license": "MIT",
|
|
35
36
|
"keywords": [
|
|
@@ -45,12 +46,12 @@
|
|
|
45
46
|
],
|
|
46
47
|
"dependencies": {
|
|
47
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
|
-
"zod": "^4.3
|
|
49
|
+
"zod": "^4.4.3"
|
|
49
50
|
},
|
|
50
51
|
"devDependencies": {
|
|
51
|
-
"@types/node": "^25.
|
|
52
|
-
"tsx": "^4.
|
|
53
|
-
"npm-check-updates": "^
|
|
52
|
+
"@types/node": "^25.9.0",
|
|
53
|
+
"tsx": "^4.22.1",
|
|
54
|
+
"npm-check-updates": "^22.2.0",
|
|
54
55
|
"typescript": "^6.0.3"
|
|
55
56
|
}
|
|
56
57
|
}
|