@owine/unifi-network-mcp 2.15.0 → 3.1.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/LICENSE +21 -0
- package/README.md +257 -77
- package/dist/client.d.ts +26 -1
- package/dist/client.js +199 -24
- package/dist/config.d.ts +11 -0
- package/dist/config.js +102 -3
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +180 -0
- package/dist/index.js +17 -4
- package/dist/instructions.d.ts +9 -0
- package/dist/instructions.js +32 -0
- package/dist/tls.d.ts +14 -0
- package/dist/tls.js +58 -0
- package/dist/tools/acl.js +123 -50
- package/dist/tools/classic.d.ts +3 -0
- package/dist/tools/classic.js +54 -0
- package/dist/tools/client-history.js +22 -12
- package/dist/tools/clients.js +10 -9
- package/dist/tools/devices.js +21 -20
- package/dist/tools/dns-policies.js +27 -27
- package/dist/tools/exposure.d.ts +3 -0
- package/dist/tools/exposure.js +118 -0
- package/dist/tools/firewall.d.ts +2 -1
- package/dist/tools/firewall.js +200 -75
- package/dist/tools/hotspot.js +35 -28
- package/dist/tools/index.d.ts +13 -1
- package/dist/tools/index.js +67 -4
- package/dist/tools/networks.d.ts +2 -1
- package/dist/tools/networks.js +44 -31
- package/dist/tools/sites.js +2 -2
- package/dist/tools/supporting.js +22 -21
- package/dist/tools/switching.js +16 -15
- package/dist/tools/system.js +1 -1
- package/dist/tools/traffic-matching.d.ts +2 -1
- package/dist/tools/traffic-matching.js +22 -20
- package/dist/tools/wifi.js +76 -30
- package/dist/utils/full-put.d.ts +11 -2
- package/dist/utils/full-put.js +25 -2
- package/dist/utils/output-schemas.d.ts +241 -4
- package/dist/utils/output-schemas.js +225 -49
- package/dist/utils/params.d.ts +12 -0
- package/dist/utils/params.js +21 -0
- package/dist/utils/pick.d.ts +6 -0
- package/dist/utils/pick.js +11 -0
- package/dist/utils/responses.d.ts +2 -0
- package/dist/utils/responses.js +28 -2
- package/dist/utils/safety.d.ts +35 -1
- package/dist/utils/safety.js +53 -3
- package/dist/utils/secrets.js +3 -0
- package/dist/utils/wan-exposure.d.ts +69 -0
- package/dist/utils/wan-exposure.js +245 -0
- package/dist/write-log.d.ts +52 -0
- package/dist/write-log.js +178 -0
- package/dist/write-tools.d.ts +27 -0
- package/dist/write-tools.js +50 -0
- package/package.json +19 -15
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WriteLog = void 0;
|
|
4
|
+
exports.noteWriteRequest = noteWriteRequest;
|
|
5
|
+
exports.noteWriteStatus = noteWriteStatus;
|
|
6
|
+
exports.noteRefusal = noteRefusal;
|
|
7
|
+
exports.localTimestamp = localTimestamp;
|
|
8
|
+
exports.openWriteLog = openWriteLog;
|
|
9
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
10
|
+
const node_fs_1 = require("node:fs");
|
|
11
|
+
const node_path_1 = require("node:path");
|
|
12
|
+
const responses_js_1 = require("./utils/responses.js");
|
|
13
|
+
const secrets_js_1 = require("./utils/secrets.js");
|
|
14
|
+
const currentCall = new node_async_hooks_1.AsyncLocalStorage();
|
|
15
|
+
const withoutQuery = (path) => path.split("?")[0];
|
|
16
|
+
/** NetworkClient: a write request passed the guard and is about to be sent. */
|
|
17
|
+
function noteWriteRequest(method, path) {
|
|
18
|
+
const call = currentCall.getStore();
|
|
19
|
+
if (call)
|
|
20
|
+
call.request = { method, path: withoutQuery(path) };
|
|
21
|
+
}
|
|
22
|
+
/** NetworkClient: the controller answered the write request. */
|
|
23
|
+
function noteWriteStatus(status) {
|
|
24
|
+
const request = currentCall.getStore()?.request;
|
|
25
|
+
if (request)
|
|
26
|
+
request.status = status;
|
|
27
|
+
}
|
|
28
|
+
/** A check refused the call; the first refusal is the one logged. */
|
|
29
|
+
function noteRefusal(reason, method, path) {
|
|
30
|
+
const call = currentCall.getStore();
|
|
31
|
+
if (call && !call.refusal)
|
|
32
|
+
call.refusal = { reason, method, ...(path === undefined ? {} : { path: withoutQuery(path) }) };
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Field names, compared without case, whose value is redacted whatever it
|
|
36
|
+
* is. Names are matched exactly, not by substring, so containers such as
|
|
37
|
+
* presharedKeys keep their network IDs (redactSecrets masks the passphrase
|
|
38
|
+
* in each entry) and fields like keyId stay readable. The list: the names
|
|
39
|
+
* redactSecrets knows from WiFi and RADIUS bodies, their legacy-API x_
|
|
40
|
+
* forms, and the usual names of credentials and key material a firewall
|
|
41
|
+
* policy record (or a mistaken argument) could carry.
|
|
42
|
+
*/
|
|
43
|
+
const SECRET_FIELDS = new Set([
|
|
44
|
+
"passphrase", "password", "secret", "sharedSecret", "radiusSecret",
|
|
45
|
+
"x_passphrase", "x_password", "x_secret", "x_shared_secret",
|
|
46
|
+
"apiKey", "api_key", "apiToken", "token", "accessToken", "refreshToken", "authToken",
|
|
47
|
+
"privateKey", "private_key", "presharedKey",
|
|
48
|
+
].map((name) => name.toLowerCase()));
|
|
49
|
+
function redactArgs(value) {
|
|
50
|
+
if (Array.isArray(value))
|
|
51
|
+
return value.map(redactArgs);
|
|
52
|
+
if (value === null || typeof value !== "object")
|
|
53
|
+
return value;
|
|
54
|
+
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, SECRET_FIELDS.has(key.toLowerCase()) ? secrets_js_1.REDACTED : redactArgs(entry)]));
|
|
55
|
+
}
|
|
56
|
+
/** "2026-10-18T21:04:05.123+02:00": the local time with its offset. */
|
|
57
|
+
function localTimestamp(date) {
|
|
58
|
+
const pad = (n, width = 2) => String(Math.abs(n)).padStart(width, "0");
|
|
59
|
+
const offset = -date.getTimezoneOffset();
|
|
60
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
61
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}` +
|
|
62
|
+
`${offset < 0 ? "-" : "+"}${pad(Math.trunc(offset / 60))}:${pad(offset % 60)}`;
|
|
63
|
+
}
|
|
64
|
+
class WriteLog {
|
|
65
|
+
sink;
|
|
66
|
+
apiKey;
|
|
67
|
+
failed = false;
|
|
68
|
+
/** sink appends one line; it may throw, and a failure is reported once and never breaks the call. */
|
|
69
|
+
constructor(sink, apiKey) {
|
|
70
|
+
this.sink = sink;
|
|
71
|
+
this.apiKey = apiKey;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Open path for appending, creating it 0600 (and its directory 0700) if
|
|
75
|
+
* needed. Throws for a symlink, anything but a regular file, or a file
|
|
76
|
+
* others can write to, which could be truncated or filled with lies.
|
|
77
|
+
*
|
|
78
|
+
* On Windows, Node reports every writable file as mode 0666 and a link
|
|
79
|
+
* count that says nothing about access, and O_NOFOLLOW does not exist, so
|
|
80
|
+
* only the symlink and regular-file checks apply there: the file's ACL is
|
|
81
|
+
* the operator's to set.
|
|
82
|
+
*/
|
|
83
|
+
static open(path, apiKey) {
|
|
84
|
+
const windows = process.platform === "win32";
|
|
85
|
+
let existing;
|
|
86
|
+
try {
|
|
87
|
+
existing = (0, node_fs_1.lstatSync)(path);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err.code !== "ENOENT")
|
|
91
|
+
throw err;
|
|
92
|
+
}
|
|
93
|
+
if (existing?.isSymbolicLink())
|
|
94
|
+
throw new Error("it is a symlink; point it at the file itself");
|
|
95
|
+
if (existing === undefined)
|
|
96
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
97
|
+
// O_NOFOLLOW closes the gap between the check above and the open.
|
|
98
|
+
const noFollow = windows ? 0 : node_fs_1.constants.O_NOFOLLOW;
|
|
99
|
+
const fd = (0, node_fs_1.openSync)(path, node_fs_1.constants.O_WRONLY | node_fs_1.constants.O_APPEND | node_fs_1.constants.O_CREAT | noFollow, 0o600);
|
|
100
|
+
const stats = (0, node_fs_1.fstatSync)(fd);
|
|
101
|
+
const problem = !stats.isFile() ? "it is not a regular file"
|
|
102
|
+
: windows ? undefined
|
|
103
|
+
: stats.nlink > 1 ? "it has other hard links, so lines could land in another file"
|
|
104
|
+
: (stats.mode & 0o022) !== 0 ? `it is group- or world-writable (mode ${(stats.mode & 0o777).toString(8)}); chmod 600 it`
|
|
105
|
+
: undefined;
|
|
106
|
+
if (problem !== undefined) {
|
|
107
|
+
(0, node_fs_1.closeSync)(fd);
|
|
108
|
+
throw new Error(problem);
|
|
109
|
+
}
|
|
110
|
+
return new WriteLog((line) => { (0, node_fs_1.writeSync)(fd, line); }, apiKey);
|
|
111
|
+
}
|
|
112
|
+
/** Run one write tool call and append its line. */
|
|
113
|
+
async record(tool, input, run) {
|
|
114
|
+
const started = new Date();
|
|
115
|
+
const call = {};
|
|
116
|
+
let result;
|
|
117
|
+
try {
|
|
118
|
+
result = await currentCall.run(call, run);
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
this.append(this.entry(tool, input, call, result, started));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
entry(tool, input, call, result, started) {
|
|
126
|
+
const dryRun = input !== null && typeof input === "object" && "dryRun" in input && input.dryRun === true;
|
|
127
|
+
const preview = result?.structuredContent?.dryRun === true ? result.structuredContent.wouldExecute : undefined;
|
|
128
|
+
const base = {
|
|
129
|
+
ts: started.toISOString(),
|
|
130
|
+
tsLocal: localTimestamp(started),
|
|
131
|
+
tool,
|
|
132
|
+
args: redactArgs((0, secrets_js_1.redactSecrets)(input)),
|
|
133
|
+
method: null,
|
|
134
|
+
path: null,
|
|
135
|
+
dryRun,
|
|
136
|
+
};
|
|
137
|
+
const durationMs = Date.now() - started.getTime();
|
|
138
|
+
if (call.refusal) {
|
|
139
|
+
const { reason, method, path } = call.refusal;
|
|
140
|
+
return { ...base, method: method ?? null, path: path ?? null, outcome: "refused", reason, durationMs };
|
|
141
|
+
}
|
|
142
|
+
if (preview) {
|
|
143
|
+
const method = typeof preview.method === "string" ? preview.method : null;
|
|
144
|
+
const path = typeof preview.path === "string" ? withoutQuery(`/integration/v1${preview.path}`) : null;
|
|
145
|
+
return { ...base, method, path, outcome: "dry_run", durationMs };
|
|
146
|
+
}
|
|
147
|
+
const request = call.request;
|
|
148
|
+
const status = request?.status === undefined ? {} : { status: request.status };
|
|
149
|
+
const where = request ? { method: request.method, path: request.path } : {};
|
|
150
|
+
return { ...base, ...where, outcome: result === undefined || result.isError ? "error" : "ok", ...status, durationMs };
|
|
151
|
+
}
|
|
152
|
+
append(entry) {
|
|
153
|
+
try {
|
|
154
|
+
let line = JSON.stringify(entry);
|
|
155
|
+
// Arguments are the model's; if it ever pasted the key into one, keep it out.
|
|
156
|
+
if (this.apiKey.length >= 8)
|
|
157
|
+
line = line.replaceAll(this.apiKey, secrets_js_1.REDACTED);
|
|
158
|
+
this.sink(`${line}\n`);
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
if (this.failed)
|
|
162
|
+
return;
|
|
163
|
+
this.failed = true;
|
|
164
|
+
console.error(`UNIFI_NETWORK_WRITE_LOG: cannot write the log (${(0, responses_js_1.errorMessage)(err)}); tool calls continue, but are not being logged.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exports.WriteLog = WriteLog;
|
|
169
|
+
/** Open the write log at startup, or exit with a configuration error. */
|
|
170
|
+
function openWriteLog(path, apiKey) {
|
|
171
|
+
try {
|
|
172
|
+
return WriteLog.open(path, apiKey);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
console.error(`Configuration error: cannot use UNIFI_NETWORK_WRITE_LOG (${path}): ${(0, responses_js_1.errorMessage)(err)}`);
|
|
176
|
+
return process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every write tool, grouped by the module in src/tools/ that registers it.
|
|
3
|
+
* UNIFI_NETWORK_WRITE_TOOLS names tools or groups from here, and
|
|
4
|
+
* registerAllTools() refuses to register a write tool that is missing, so the
|
|
5
|
+
* list cannot fall behind the code.
|
|
6
|
+
*/
|
|
7
|
+
export declare const WRITE_TOOL_GROUPS: {
|
|
8
|
+
readonly devices: readonly ["unifi_adopt_device", "unifi_remove_device", "unifi_restart_device", "unifi_power_cycle_port"];
|
|
9
|
+
readonly clients: readonly ["unifi_authorize_guest", "unifi_unauthorize_guest"];
|
|
10
|
+
readonly networks: readonly ["unifi_create_network", "unifi_update_network", "unifi_delete_network"];
|
|
11
|
+
readonly wifi: readonly ["unifi_create_wifi", "unifi_update_wifi", "unifi_delete_wifi"];
|
|
12
|
+
readonly hotspot: readonly ["unifi_create_voucher", "unifi_delete_voucher", "unifi_bulk_delete_vouchers"];
|
|
13
|
+
readonly firewall: readonly ["unifi_create_firewall_zone", "unifi_update_firewall_zone", "unifi_delete_firewall_zone", "unifi_create_firewall_policy", "unifi_update_firewall_policy", "unifi_patch_firewall_policy", "unifi_delete_firewall_policy", "unifi_reorder_firewall_policies"];
|
|
14
|
+
readonly acl: readonly ["unifi_create_acl_rule", "unifi_update_acl_rule", "unifi_delete_acl_rule", "unifi_reorder_acl_rules"];
|
|
15
|
+
readonly "dns-policies": readonly ["unifi_create_dns_policy", "unifi_update_dns_policy", "unifi_delete_dns_policy"];
|
|
16
|
+
readonly "traffic-matching": readonly ["unifi_create_traffic_matching_list", "unifi_update_traffic_matching_list", "unifi_delete_traffic_matching_list"];
|
|
17
|
+
};
|
|
18
|
+
export declare const WRITE_TOOLS: readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* The write tools a UNIFI_NETWORK_WRITE_TOOLS value enables, in catalog
|
|
21
|
+
* order: a comma-separated list of tool and group names, or "" / "none" for
|
|
22
|
+
* none. Names that are neither come back in unknown.
|
|
23
|
+
*/
|
|
24
|
+
export declare function parseWriteTools(value: string): {
|
|
25
|
+
tools: string[];
|
|
26
|
+
unknown: string[];
|
|
27
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WRITE_TOOLS = exports.WRITE_TOOL_GROUPS = void 0;
|
|
4
|
+
exports.parseWriteTools = parseWriteTools;
|
|
5
|
+
/**
|
|
6
|
+
* Every write tool, grouped by the module in src/tools/ that registers it.
|
|
7
|
+
* UNIFI_NETWORK_WRITE_TOOLS names tools or groups from here, and
|
|
8
|
+
* registerAllTools() refuses to register a write tool that is missing, so the
|
|
9
|
+
* list cannot fall behind the code.
|
|
10
|
+
*/
|
|
11
|
+
exports.WRITE_TOOL_GROUPS = {
|
|
12
|
+
devices: ["unifi_adopt_device", "unifi_remove_device", "unifi_restart_device", "unifi_power_cycle_port"],
|
|
13
|
+
clients: ["unifi_authorize_guest", "unifi_unauthorize_guest"],
|
|
14
|
+
networks: ["unifi_create_network", "unifi_update_network", "unifi_delete_network"],
|
|
15
|
+
wifi: ["unifi_create_wifi", "unifi_update_wifi", "unifi_delete_wifi"],
|
|
16
|
+
hotspot: ["unifi_create_voucher", "unifi_delete_voucher", "unifi_bulk_delete_vouchers"],
|
|
17
|
+
firewall: [
|
|
18
|
+
"unifi_create_firewall_zone", "unifi_update_firewall_zone", "unifi_delete_firewall_zone",
|
|
19
|
+
"unifi_create_firewall_policy", "unifi_update_firewall_policy", "unifi_patch_firewall_policy",
|
|
20
|
+
"unifi_delete_firewall_policy", "unifi_reorder_firewall_policies",
|
|
21
|
+
],
|
|
22
|
+
acl: ["unifi_create_acl_rule", "unifi_update_acl_rule", "unifi_delete_acl_rule", "unifi_reorder_acl_rules"],
|
|
23
|
+
"dns-policies": ["unifi_create_dns_policy", "unifi_update_dns_policy", "unifi_delete_dns_policy"],
|
|
24
|
+
"traffic-matching": [
|
|
25
|
+
"unifi_create_traffic_matching_list", "unifi_update_traffic_matching_list", "unifi_delete_traffic_matching_list",
|
|
26
|
+
],
|
|
27
|
+
};
|
|
28
|
+
exports.WRITE_TOOLS = Object.values(exports.WRITE_TOOL_GROUPS).flat();
|
|
29
|
+
const GROUPS = exports.WRITE_TOOL_GROUPS;
|
|
30
|
+
/**
|
|
31
|
+
* The write tools a UNIFI_NETWORK_WRITE_TOOLS value enables, in catalog
|
|
32
|
+
* order: a comma-separated list of tool and group names, or "" / "none" for
|
|
33
|
+
* none. Names that are neither come back in unknown.
|
|
34
|
+
*/
|
|
35
|
+
function parseWriteTools(value) {
|
|
36
|
+
const names = value.split(",").map((name) => name.trim()).filter((name) => name !== "");
|
|
37
|
+
if (names.length === 1 && names[0].toLowerCase() === "none")
|
|
38
|
+
return { tools: [], unknown: [] };
|
|
39
|
+
const enabled = new Set();
|
|
40
|
+
const unknown = [];
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const tools = Object.hasOwn(GROUPS, name) ? GROUPS[name] : exports.WRITE_TOOLS.includes(name) ? [name] : undefined;
|
|
43
|
+
if (tools === undefined)
|
|
44
|
+
unknown.push(name);
|
|
45
|
+
else
|
|
46
|
+
for (const tool of tools)
|
|
47
|
+
enabled.add(tool);
|
|
48
|
+
}
|
|
49
|
+
return { tools: exports.WRITE_TOOLS.filter((tool) => enabled.has(tool)), unknown };
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owine/unifi-network-mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "MCP server exposing the UniFi Network Integration API as tools for Claude and other MCP clients",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"unifi-network-mcp": "dist/index.js"
|
|
@@ -12,17 +12,18 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"build": "tsc",
|
|
14
14
|
"start": "node dist/index.js",
|
|
15
|
-
"typecheck": "tsc --noEmit",
|
|
15
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
16
16
|
"test": "vitest run",
|
|
17
17
|
"test:watch": "vitest",
|
|
18
18
|
"test:coverage": "vitest run --coverage",
|
|
19
19
|
"lint": "eslint .",
|
|
20
|
-
"lint:fix": "eslint . --fix"
|
|
20
|
+
"lint:fix": "eslint . --fix",
|
|
21
|
+
"test:package": "pnpm run build && sh scripts/smoke-package.sh"
|
|
21
22
|
},
|
|
22
23
|
"engines": {
|
|
23
|
-
"node": "^22.
|
|
24
|
+
"node": "^22.19.0 || ^24.0.0 || ^26.0.0"
|
|
24
25
|
},
|
|
25
|
-
"packageManager": "pnpm@12.
|
|
26
|
+
"packageManager": "pnpm@12.16.1",
|
|
26
27
|
"repository": {
|
|
27
28
|
"type": "git",
|
|
28
29
|
"url": "git+https://github.com/owine/unifi-network-mcp.git"
|
|
@@ -32,23 +33,26 @@
|
|
|
32
33
|
"network",
|
|
33
34
|
"mcp",
|
|
34
35
|
"model-context-protocol",
|
|
35
|
-
"ubiquiti"
|
|
36
|
+
"ubiquiti",
|
|
37
|
+
"unifi-network",
|
|
38
|
+
"mcp-server"
|
|
36
39
|
],
|
|
37
40
|
"author": "owine",
|
|
38
41
|
"license": "MIT",
|
|
39
42
|
"dependencies": {
|
|
40
|
-
"@modelcontextprotocol/sdk": "1.32.
|
|
41
|
-
"
|
|
43
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
44
|
+
"undici": "8.12.3",
|
|
45
|
+
"zod": "4.6.7"
|
|
42
46
|
},
|
|
43
47
|
"devDependencies": {
|
|
44
48
|
"@eslint/js": "10.0.1",
|
|
45
|
-
"@types/node": "
|
|
49
|
+
"@types/node": "26.6.6",
|
|
46
50
|
"@typescript/native": "npm:typescript@7.0.2",
|
|
47
|
-
"@vitest/coverage-v8": "5.0.
|
|
48
|
-
"eslint": "10.
|
|
49
|
-
"lefthook": "2.2.
|
|
51
|
+
"@vitest/coverage-v8": "5.0.5",
|
|
52
|
+
"eslint": "10.14.0",
|
|
53
|
+
"lefthook": "2.2.2",
|
|
50
54
|
"typescript": "npm:@typescript/typescript6@6.0.2",
|
|
51
|
-
"typescript-eslint": "8.
|
|
52
|
-
"vitest": "5.0.
|
|
55
|
+
"typescript-eslint": "8.72.1",
|
|
56
|
+
"vitest": "5.0.5"
|
|
53
57
|
}
|
|
54
58
|
}
|