aether-mcp-server 2.0.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.
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.McpTaskMemory = void 0;
4
+ const MAX_MEMORY = 100;
5
+ class McpTaskMemory {
6
+ tasks = [];
7
+ sessions = [];
8
+ create(action, url, parentId) {
9
+ const node = {
10
+ id: `v2-${Math.random().toString(36).substring(7)}`,
11
+ action,
12
+ url,
13
+ timestamp: new Date().toISOString(),
14
+ parentId,
15
+ status: "pending",
16
+ };
17
+ this.tasks.push(node);
18
+ this.trim(this.tasks);
19
+ return node;
20
+ }
21
+ recordSession(item) {
22
+ this.sessions.unshift({ timestamp: new Date().toISOString(), ...item });
23
+ this.trim(this.sessions);
24
+ }
25
+ graph() {
26
+ return this.tasks;
27
+ }
28
+ history() {
29
+ return this.sessions;
30
+ }
31
+ trim(items) {
32
+ while (items.length > MAX_MEMORY)
33
+ items.shift();
34
+ }
35
+ }
36
+ exports.McpTaskMemory = McpTaskMemory;
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PageSnapshotCache = void 0;
4
+ const CACHE_TTL_MS = 1000;
5
+ class PageSnapshotCache {
6
+ client;
7
+ locator;
8
+ version = 0;
9
+ cached = null;
10
+ constructor(client, locator) {
11
+ this.client = client;
12
+ this.locator = locator;
13
+ this.installInvalidationHooks();
14
+ }
15
+ invalidate(reason = "manual") {
16
+ this.version++;
17
+ this.cached = null;
18
+ console.error(`[SnapshotCache] invalidated: ${reason}`);
19
+ }
20
+ async compact(params = {}) {
21
+ const maxElements = Math.max(0, Math.min(Number(params.maxElements ?? 30), 200));
22
+ const now = Date.now();
23
+ const cached = this.cached;
24
+ if (cached && cached.version === this.version && now - cached.createdAt <= CACHE_TTL_MS && (maxElements === 0 || cached.hasElements)) {
25
+ return {
26
+ title: cached.title,
27
+ url: cached.url,
28
+ readyState: cached.readyState,
29
+ elementCount: cached.elements.length,
30
+ elements: cached.elements.slice(0, maxElements),
31
+ cache: { hit: true, version: cached.version, ageMs: now - cached.createdAt },
32
+ };
33
+ }
34
+ const [facts, elements] = await Promise.all([
35
+ this.client.evaluate(`({
36
+ title: document.title || "Unknown",
37
+ url: window.location.href || "Unknown",
38
+ readyState: document.readyState || "unknown"
39
+ })`).catch(() => ({ title: "Unknown", url: "Unknown", readyState: "unknown" })),
40
+ maxElements === 0
41
+ ? Promise.resolve([])
42
+ : this.locator.list(200, params.includeText !== false, !!params.withOverlay).catch(() => []),
43
+ ]);
44
+ this.cached = {
45
+ version: this.version,
46
+ createdAt: Date.now(),
47
+ hasElements: maxElements > 0,
48
+ title: String(facts?.title ?? "Unknown"),
49
+ url: String(facts?.url ?? "Unknown"),
50
+ readyState: String(facts?.readyState ?? "unknown"),
51
+ elements,
52
+ };
53
+ return {
54
+ title: this.cached.title,
55
+ url: this.cached.url,
56
+ readyState: this.cached.readyState,
57
+ elementCount: elements.length,
58
+ elements: elements.slice(0, maxElements),
59
+ cache: { hit: false, version: this.cached.version, ageMs: 0 },
60
+ };
61
+ }
62
+ installInvalidationHooks() {
63
+ const events = [
64
+ "DOM.documentUpdated",
65
+ "Page.frameNavigated",
66
+ "Page.loadEventFired",
67
+ "Runtime.executionContextDestroyed",
68
+ "Runtime.executionContextsCleared",
69
+ ];
70
+ for (const event of events) {
71
+ this.client.on(event, () => this.invalidate(event));
72
+ }
73
+ }
74
+ }
75
+ exports.PageSnapshotCache = PageSnapshotCache;
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.planWithLocalPolicy = planWithLocalPolicy;
4
+ const ALLOWED_INTENTS = new Set([
5
+ "navigate",
6
+ "click",
7
+ "fill",
8
+ "select",
9
+ "check",
10
+ "wait_for",
11
+ "inspect",
12
+ ]);
13
+ function extractJson(text) {
14
+ const trimmed = text.trim();
15
+ try {
16
+ return JSON.parse(trimmed);
17
+ }
18
+ catch { }
19
+ const start = trimmed.indexOf("{");
20
+ const end = trimmed.lastIndexOf("}");
21
+ if (start >= 0 && end > start) {
22
+ return JSON.parse(trimmed.slice(start, end + 1));
23
+ }
24
+ throw new Error("Policy model did not return JSON.");
25
+ }
26
+ function validatePlan(raw) {
27
+ if (!raw || typeof raw !== "object") {
28
+ throw new Error("Policy plan must be an object.");
29
+ }
30
+ const intent = String(raw.intent || "").toLowerCase();
31
+ if (!ALLOWED_INTENTS.has(intent)) {
32
+ throw new Error(`Unsupported policy intent: ${raw.intent}`);
33
+ }
34
+ const plan = { intent };
35
+ for (const key of ["target", "value", "role", "verify", "reason"]) {
36
+ if (raw[key] !== undefined && raw[key] !== null)
37
+ plan[key] = String(raw[key]);
38
+ }
39
+ if (raw.confidence !== undefined) {
40
+ const confidence = Number(raw.confidence);
41
+ if (Number.isFinite(confidence))
42
+ plan.confidence = Math.max(0, Math.min(1, confidence));
43
+ }
44
+ if (["click", "fill", "select", "check"].includes(intent) && !plan.target) {
45
+ throw new Error(`${intent} policy plan requires target.`);
46
+ }
47
+ if (["navigate", "fill", "select", "wait_for"].includes(intent) && !plan.value) {
48
+ throw new Error(`${intent} policy plan requires value.`);
49
+ }
50
+ return plan;
51
+ }
52
+ function buildPrompt(request) {
53
+ return [
54
+ "You are a tiny browser policy model.",
55
+ "Convert the user instruction and compact page snapshot into exactly one JSON action.",
56
+ "Return only JSON. No markdown. No prose.",
57
+ "",
58
+ "Allowed schema:",
59
+ '{"intent":"navigate|click|fill|select|check|wait_for|inspect","target":"visible label or CSS selector","value":"text/url/option when needed","role":"button|link|textbox|checkbox|combobox when useful","verify":"optional visible text to wait for","confidence":0.0,"reason":"short"}',
60
+ "",
61
+ "Rules:",
62
+ "- Prefer visible text, aria labels, labels, placeholders, or stable CSS selectors from the snapshot.",
63
+ "- Use navigate only when the instruction contains a URL or explicit page destination.",
64
+ "- Use inspect when the instruction asks only to observe/read.",
65
+ "- Do not invent hidden fields or credentials.",
66
+ "",
67
+ `Instruction: ${request.instruction}`,
68
+ "",
69
+ `Snapshot JSON: ${JSON.stringify(request.snapshot).slice(0, 12000)}`,
70
+ ].join("\n");
71
+ }
72
+ async function planWithLocalPolicy(request) {
73
+ const endpoint = process.env.AETHER_POLICY_ENDPOINT || "http://127.0.0.1:11434/v1/chat/completions";
74
+ const model = request.model || process.env.AETHER_POLICY_MODEL || "local-browser-policy";
75
+ const prompt = buildPrompt(request);
76
+ const response = await fetch(endpoint, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({
80
+ model,
81
+ temperature: 0,
82
+ messages: [
83
+ {
84
+ role: "system",
85
+ content: "Return one strict JSON object for the next browser action.",
86
+ },
87
+ {
88
+ role: "user",
89
+ content: prompt,
90
+ },
91
+ ],
92
+ }),
93
+ });
94
+ if (!response.ok) {
95
+ const body = await response.text().catch(() => "");
96
+ throw new Error(`Policy model request failed (${response.status}): ${body.slice(0, 300)}`);
97
+ }
98
+ const payload = await response.json();
99
+ const content = payload?.choices?.[0]?.message?.content ?? payload?.message?.content ?? payload?.response;
100
+ if (typeof content !== "string") {
101
+ throw new Error("Policy model response did not include message content.");
102
+ }
103
+ return validatePlan(extractJson(content));
104
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.STEALTH_SCRIPT = void 0;
4
+ /**
5
+ * Comprehensive stealth script to bypass bot detection.
6
+ * Inspired by puppeteer-extra-plugin-stealth and modern bot bypass techniques.
7
+ */
8
+ exports.STEALTH_SCRIPT = `
9
+ (function() {
10
+ const safely = (patch) => {
11
+ try {
12
+ patch();
13
+ } catch (error) {
14
+ // Stealth must never break the page's own boot scripts.
15
+ console.debug('[Aether stealth] patch skipped', error);
16
+ }
17
+ };
18
+
19
+ // Helper to make overridden functions look native
20
+ const makeNative = (fn, name) => {
21
+ Object.defineProperty(fn, 'name', { value: name, configurable: true });
22
+ Object.defineProperty(fn, 'toString', {
23
+ value: () => \`function \${name}() { [native code] }\`,
24
+ configurable: true,
25
+ writable: true
26
+ });
27
+ };
28
+
29
+ // Helper to mock getters native-style
30
+ const mockGetter = (obj, prop, value) => {
31
+ if (!obj) return;
32
+ const desc = Object.getOwnPropertyDescriptor(obj, prop) || {
33
+ configurable: true,
34
+ enumerable: true
35
+ };
36
+ if (desc.configurable === false) return;
37
+ const getter = () => value;
38
+ makeNative(getter, \`get \${prop}\`);
39
+ Object.defineProperty(obj, prop, {
40
+ ...desc,
41
+ get: getter
42
+ });
43
+ };
44
+
45
+ // 1. Remove navigator.webdriver (or set to undefined native-style)
46
+ safely(() => mockGetter(navigator, 'webdriver', undefined));
47
+
48
+ // 2. Mock chrome.runtime without replacing existing Chrome globals.
49
+ safely(() => {
50
+ const chromeObj = window.chrome || {};
51
+ chromeObj.runtime = chromeObj.runtime || {};
52
+ Object.assign(chromeObj.runtime, {
53
+ OnInstalledReason: { INSTALL: 'install', UPDATE: 'update', CHROME_UPDATE: 'chrome_update', SHARED_MODULE_UPDATE: 'shared_module_update' },
54
+ OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
55
+ PlatformArch: { ARM: 'arm', ARM64: 'arm64', X86_32: 'x86-32', X86_64: 'x86-64' },
56
+ PlatformNaclArch: { ARM: 'arm', X86_32: 'x86-32', X86_64: 'x86-64' },
57
+ PlatformOs: { ANDROID: 'android', CROS: 'cros', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
58
+ RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', UPDATE_AVAILABLE: 'update_available', THROTTLED: 'throttled' }
59
+ });
60
+ window.chrome = chromeObj;
61
+ });
62
+
63
+ // 3. Spoof navigator.languages
64
+ safely(() => mockGetter(navigator, 'languages', ['en-US', 'en']));
65
+
66
+ // 4. Spoof navigator.plugins
67
+ safely(() => mockGetter(navigator, 'plugins', (() => {
68
+ const plugins = [
69
+ { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
70
+ { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
71
+ { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
72
+ { name: 'Microsoft Edge PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
73
+ { name: 'WebKit built-in PDF', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }
74
+ ];
75
+ const p = [...plugins];
76
+ p.item = (i) => plugins[i];
77
+ p.namedItem = (n) => plugins.find(x => x.name === n);
78
+ p.refresh = () => {};
79
+
80
+ makeNative(p.item, 'item');
81
+ makeNative(p.namedItem, 'namedItem');
82
+ makeNative(p.refresh, 'refresh');
83
+
84
+ return p;
85
+ })()));
86
+
87
+ // 5. Overcome WebGL fingerprinting
88
+ safely(() => {
89
+ const contexts = [window.WebGLRenderingContext, window.WebGL2RenderingContext].filter(Boolean);
90
+ for (const Context of contexts) {
91
+ const getParameter = Context.prototype.getParameter;
92
+ if (typeof getParameter !== 'function') continue;
93
+ Context.prototype.getParameter = function(parameter) {
94
+ // UNMASKED_VENDOR_WEBGL
95
+ if (parameter === 37445) return 'Intel Inc.';
96
+ // UNMASKED_RENDERER_WEBGL
97
+ if (parameter === 37446) return 'Intel(R) Iris(TM) Graphics 6100';
98
+ return getParameter.apply(this, arguments);
99
+ };
100
+ makeNative(Context.prototype.getParameter, 'getParameter');
101
+ }
102
+ });
103
+
104
+ // 6. Fix navigator.permissions.query
105
+ safely(() => {
106
+ const permissions = window.navigator.permissions;
107
+ if (!permissions || typeof permissions.query !== 'function') return;
108
+ const originalQuery = permissions.query.bind(permissions);
109
+ permissions.query = (parameters) => (
110
+ parameters && parameters.name === 'notifications' ?
111
+ Promise.resolve({ state: Notification.permission }) :
112
+ originalQuery(parameters)
113
+ );
114
+ makeNative(permissions.query, 'query');
115
+ });
116
+
117
+ // 7. Mock navigator.deviceMemory
118
+ safely(() => {
119
+ if (!navigator.deviceMemory) {
120
+ mockGetter(navigator, 'deviceMemory', 8);
121
+ }
122
+ });
123
+
124
+ // 8. Mock hardwareConcurrency
125
+ safely(() => {
126
+ if (!navigator.hardwareConcurrency) {
127
+ mockGetter(navigator, 'hardwareConcurrency', 4);
128
+ }
129
+ });
130
+
131
+ // 9. Add window.outerHeight and window.outerWidth if missing (common in headless)
132
+ safely(() => {
133
+ if (window.outerHeight === 0) window.outerHeight = window.innerHeight;
134
+ if (window.outerWidth === 0) window.outerWidth = window.innerWidth;
135
+ });
136
+ })();
137
+ `;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.recordPolicyTrace = recordPolicyTrace;
7
+ exports.recordPolicyFeedback = recordPolicyFeedback;
8
+ const promises_1 = require("fs/promises");
9
+ const path_1 = __importDefault(require("path"));
10
+ const SECRET_KEYS = /password|passwd|secret|token|cookie|authorization|api[-_]?key|client[-_]?key/i;
11
+ function redact(value, key = "") {
12
+ if (SECRET_KEYS.test(key))
13
+ return "[REDACTED]";
14
+ if (Array.isArray(value))
15
+ return value.map((item) => redact(item));
16
+ if (!value || typeof value !== "object")
17
+ return value;
18
+ return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [entryKey, redact(entryValue, entryKey)]));
19
+ }
20
+ function tracePath() {
21
+ return process.env.AETHER_POLICY_TRACE_PATH ||
22
+ path_1.default.join(process.cwd(), "data", "policy-traces.jsonl");
23
+ }
24
+ function feedbackPath() {
25
+ return process.env.AETHER_POLICY_FEEDBACK_PATH ||
26
+ path_1.default.join(process.cwd(), "data", "policy-feedback.jsonl");
27
+ }
28
+ function createTraceId() {
29
+ return `pol_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
30
+ }
31
+ async function recordPolicyTrace(trace) {
32
+ const id = trace.id || createTraceId();
33
+ if (process.env.AETHER_RECORD_POLICY_TRACES === "false")
34
+ return id;
35
+ const filePath = tracePath();
36
+ const safeTrace = redact({ ...trace, id });
37
+ await (0, promises_1.mkdir)(path_1.default.dirname(filePath), { recursive: true });
38
+ await (0, promises_1.appendFile)(filePath, `${JSON.stringify(safeTrace)}\n`, "utf8");
39
+ return id;
40
+ }
41
+ async function recordPolicyFeedback(feedback) {
42
+ const filePath = feedbackPath();
43
+ const safeFeedback = redact(feedback);
44
+ await (0, promises_1.mkdir)(path_1.default.dirname(filePath), { recursive: true });
45
+ await (0, promises_1.appendFile)(filePath, `${JSON.stringify(safeFeedback)}\n`, "utf8");
46
+ }
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.activeConnection = void 0;
7
+ exports.ensurePortAvailable = ensurePortAvailable;
8
+ exports.StartWebSocketServer = StartWebSocketServer;
9
+ exports.sendCommandToExtension = sendCommandToExtension;
10
+ const ws_1 = require("ws");
11
+ const http_1 = __importDefault(require("http"));
12
+ exports.activeConnection = null;
13
+ let messageIdCounter = 0;
14
+ const pendingRequests = new Map();
15
+ /**
16
+ * Ensure port is available by killing any stale server
17
+ */
18
+ async function ensurePortAvailable(port) {
19
+ return new Promise((resolve) => {
20
+ const req = http_1.default.get(`http://localhost:${port}/shutdown`, (res) => {
21
+ console.error(`[WS] Sent shutdown to existing server on port ${port}`);
22
+ setTimeout(resolve, 1500);
23
+ });
24
+ req.on("error", () => {
25
+ resolve();
26
+ });
27
+ req.setTimeout(2000, () => {
28
+ req.destroy();
29
+ resolve();
30
+ });
31
+ });
32
+ }
33
+ function StartWebSocketServer(port) {
34
+ let retryCount = 0;
35
+ const MAX_RETRIES = 3;
36
+ const server = http_1.default.createServer((req, res) => {
37
+ if (req.url === "/health") {
38
+ res.writeHead(200, { "Content-Type": "application/json" });
39
+ res.end(JSON.stringify({
40
+ status: "ok",
41
+ connected: exports.activeConnection !== null,
42
+ pid: process.pid,
43
+ }));
44
+ }
45
+ else if (req.url === "/shutdown") {
46
+ res.writeHead(200, { "Content-Type": "text/plain" });
47
+ res.end("shutting down");
48
+ console.error("[WS] Received shutdown request. Exiting...");
49
+ setTimeout(() => process.exit(0), 500);
50
+ }
51
+ else {
52
+ res.writeHead(404);
53
+ res.end();
54
+ }
55
+ });
56
+ const wss = new ws_1.WebSocketServer({ server });
57
+ server.on("error", (err) => {
58
+ if (err.code === "EADDRINUSE") {
59
+ if (retryCount >= MAX_RETRIES) {
60
+ console.error(`[WS] Port ${port} in use. Max retries reached. Exiting.`);
61
+ process.exit(1);
62
+ }
63
+ retryCount++;
64
+ console.error(`[WS] Port ${port} in use. Retry ${retryCount}/${MAX_RETRIES} after 2s...`);
65
+ setTimeout(() => {
66
+ server.close();
67
+ StartWebSocketServer(port);
68
+ }, 2000);
69
+ }
70
+ else {
71
+ console.error("[WS] Server error:", err.message);
72
+ }
73
+ });
74
+ server.listen(port, () => {
75
+ console.error(`[WS] HTTP/WebSocket Server listening on port ${port}`);
76
+ retryCount = 0; // Reset on success
77
+ });
78
+ wss.on("connection", (ws) => {
79
+ console.error("[WS] Extension connected");
80
+ exports.activeConnection = ws;
81
+ ws.on("message", (data) => {
82
+ try {
83
+ const message = JSON.parse(data.toString());
84
+ if (message.id !== undefined && pendingRequests.has(message.id)) {
85
+ const { resolve, reject } = pendingRequests.get(message.id);
86
+ if (message.error) {
87
+ reject(new Error(message.error));
88
+ }
89
+ else {
90
+ resolve(message.result);
91
+ }
92
+ pendingRequests.delete(message.id);
93
+ }
94
+ else if (message.method === "ping") {
95
+ // Heartbeat — ignore
96
+ }
97
+ }
98
+ catch (err) {
99
+ console.error("[WS] Parse error:", err);
100
+ }
101
+ });
102
+ ws.on("close", () => {
103
+ console.error("[WS] Extension disconnected");
104
+ if (exports.activeConnection === ws) {
105
+ exports.activeConnection = null;
106
+ }
107
+ for (const [id, { reject }] of pendingRequests) {
108
+ reject(new Error("Extension disconnected"));
109
+ }
110
+ pendingRequests.clear();
111
+ });
112
+ ws.on("error", (err) => {
113
+ console.error("[WS] Connection error:", err.message);
114
+ });
115
+ });
116
+ return wss;
117
+ }
118
+ function sendCommandToExtension(method, params) {
119
+ return new Promise((resolve, reject) => {
120
+ if (!exports.activeConnection || exports.activeConnection.readyState !== ws_1.WebSocket.OPEN) {
121
+ return reject(new Error("No active extension connection"));
122
+ }
123
+ const id = ++messageIdCounter;
124
+ const timeout = setTimeout(() => {
125
+ pendingRequests.delete(id);
126
+ reject(new Error(`Command '${method}' timed out after 30s`));
127
+ }, 30000);
128
+ pendingRequests.set(id, {
129
+ resolve: (val) => { clearTimeout(timeout); resolve(val); },
130
+ reject: (err) => { clearTimeout(timeout); reject(err); },
131
+ });
132
+ exports.activeConnection.send(JSON.stringify({ id, method, params }));
133
+ });
134
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "aether-mcp-server",
3
+ "version": "2.0.0",
4
+ "description": "Aether MCP Server - AI Browser Controller",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "aether-mcp-server": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "postbuild": "node -e \"const f=require('fs');const c=f.readFileSync('dist/index.js','utf8');f.writeFileSync('dist/index.js','#!/usr/bin/env node\\n'+c.replace('#!/usr/bin/env node\\n',''))\"",
15
+ "start": "ts-node src/index.ts",
16
+ "dev": "nodemon src/index.ts",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [],
20
+ "author": "",
21
+ "license": "ISC",
22
+ "dependencies": {
23
+ "@modelcontextprotocol/sdk": "^0.6.0",
24
+ "ws": "^8.18.0",
25
+ "zod": "^3.23.8"
26
+ },
27
+ "devDependencies": {
28
+ "@types/express": "^5.0.0",
29
+ "@types/node": "^22.5.5",
30
+ "@types/ws": "^8.5.12",
31
+ "nodemon": "^3.1.4",
32
+ "ts-node": "^10.9.2",
33
+ "typescript": "^5.6.2"
34
+ }
35
+ }