@termhub/agent 0.3.0 → 0.4.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.
Files changed (2) hide show
  1. package/dist/cli.js +309 -23
  2. package/package.json +2 -1
package/dist/cli.js CHANGED
@@ -93,6 +93,8 @@ var PROTOCOL_VERSION = 1;
93
93
  var CLOSE = { UNAUTHORIZED: 4401, CONFLICT: 4409, VIOLATION: 1008 };
94
94
  var channel = z2.number().int().min(1).max(4294967295);
95
95
  var rpcId = z2.string().min(1).max(64);
96
+ var closedReason = z2.enum(["cli_missing", "run_failed", "killed", "missing_session", "cli_rejected"]);
97
+ var CAPABILITY_CLAUDE = "claude";
96
98
  var helloMessage = z2.object({
97
99
  type: z2.literal("hello"),
98
100
  protocol: z2.number().int().min(1),
@@ -105,19 +107,44 @@ var helloMessage = z2.object({
105
107
  /** `status`/`doctor` reachability check: the server validates the token and protocol as
106
108
  * usual but does not attach — it answers by closing 1000 `probe-ok`, so a probe never
107
109
  * replaces the machine's live session. */
108
- probe: z2.boolean().optional()
110
+ probe: z2.boolean().optional(),
111
+ // What this agent understands beyond the baseline `pty` channel (e.g. `claude`, for a
112
+ // headless Claude run). Optional, defaulting to [] — every agent already in the field sent
113
+ // `hello` before this field existed, so it must keep parsing, reading as "no capabilities"
114
+ // rather than failing validation or coming back `undefined`. The server decides whether it
115
+ // may open a `claude` channel from this list, before it ever tries.
116
+ capabilities: z2.array(z2.string().max(64)).max(32).default([])
109
117
  });
110
118
  var agentMessage = z2.discriminatedUnion("type", [
111
119
  helloMessage,
112
120
  z2.object({ type: z2.literal("rpc_result"), id: rpcId, ok: z2.boolean(), result: z2.unknown().optional(), error: rpcErrorSchema.optional() }),
113
121
  z2.object({ type: z2.literal("opened"), ch: channel }),
114
122
  z2.object({ type: z2.literal("open_error"), ch: channel, error: rpcErrorSchema }),
115
- z2.object({ type: z2.literal("closed"), ch: channel, code: z2.number().int().nullable() })
123
+ // `reason` is absent for a clean exit and for every pty close today; a headless claude run
124
+ // (task 3) sets it so the server can render the same failure the same way every time.
125
+ z2.object({ type: z2.literal("closed"), ch: channel, code: z2.number().int().nullable(), reason: closedReason.optional() })
116
126
  ]);
117
127
  var ptyOpenParams = z2.object({ session: sessionName, cwd: machinePath, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) });
118
- var serverMessage = z2.discriminatedUnion("type", [
128
+ var claudeOpenParams = z2.object({
129
+ session_id: z2.string(),
130
+ resume: z2.boolean(),
131
+ // null selects the machine's default Claude account; a path names a specific one.
132
+ config_dir: z2.string().nullable(),
133
+ mcp_url: z2.string(),
134
+ // The user's own token for this run, minted by the server per run and revoked the moment
135
+ // the next one starts. It travels here, in the frame that opens the channel, rather than
136
+ // over the channel itself, because the channel doesn't exist yet when the agent needs it —
137
+ // opening a channel to the user's own machine is exactly the trust boundary this grant
138
+ // belongs at, so there is no separate, later place to hand it over.
139
+ token: z2.string(),
140
+ model: z2.string().nullable().optional()
141
+ });
142
+ var openPty = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams });
143
+ var openClaude = z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("claude"), params: claudeOpenParams });
144
+ var serverMessage = z2.union([
119
145
  z2.object({ type: z2.literal("rpc"), id: rpcId, method: rpcMethod, params: z2.unknown() }),
120
- z2.object({ type: z2.literal("open"), ch: channel, kind: z2.literal("pty"), params: ptyOpenParams }),
146
+ openPty,
147
+ openClaude,
121
148
  z2.object({ type: z2.literal("resize"), ch: channel, cols: z2.number().int().min(2).max(500), rows: z2.number().int().min(2).max(200) }),
122
149
  z2.object({ type: z2.literal("close"), ch: channel })
123
150
  ]);
@@ -766,8 +793,8 @@ import { execFile } from "child_process";
766
793
  import os2 from "os";
767
794
  var DEFAULT_TIMEOUT_MS = 8e3;
768
795
  var RpcFailure = class extends Error {
769
- constructor(code, message, path12) {
770
- super(message);
796
+ constructor(code, message2, path12) {
797
+ super(message2);
771
798
  this.code = code;
772
799
  this.path = path12;
773
800
  }
@@ -863,8 +890,8 @@ async function install(params, home = os3.homedir()) {
863
890
  try {
864
891
  merged.push({ target, body: mergeClaudeSettings(await readOrEmpty2(target.file), scriptPath) });
865
892
  } catch (err) {
866
- const message = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `${target.shown} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
867
- throw new RpcFailure("failed", message, relPath(target.shown));
893
+ const message2 = err instanceof SyntaxError || err instanceof Error && err.message.includes("n\xE3o \xE9 um objeto JSON") ? `${target.shown} n\xE3o \xE9 JSON v\xE1lido` : err instanceof Error ? err.message : String(err);
894
+ throw new RpcFailure("failed", message2, relPath(target.shown));
868
895
  }
869
896
  }
870
897
  const hasCodex = await isDir2(path3.join(home, CODEX_DIR_REL));
@@ -946,6 +973,252 @@ async function uninstall(params, home = os3.homedir()) {
946
973
  return { removed: true };
947
974
  }
948
975
 
976
+ // ../../packages/claude-cli/dist/index.js
977
+ var DISALLOWED_TOOLS = "Bash,Read,Write,Edit,WebFetch,WebSearch";
978
+ function buildClaudeArgs(spec) {
979
+ return [
980
+ "-p",
981
+ // Exactly one of the two, never both: the CLI answers "--session-id can only be used with
982
+ // --continue or --resume if --fork-session is also specified" and exits 1 before doing any
983
+ // work, which broke every message after the first. --session-id is how the server names a new
984
+ // session; --resume is how it continues one it already named.
985
+ ...spec.resume ? ["--resume", spec.session_id] : ["--session-id", spec.session_id],
986
+ "--output-format",
987
+ "stream-json",
988
+ // required by the CLI: with --print, --output-format=stream-json refuses to run without it
989
+ // ("Error: When using --print, --output-format=stream-json requires --verbose"). It only
990
+ // changes what the CLI writes to stdout, never logging the prompt.
991
+ "--verbose",
992
+ "--include-partial-messages",
993
+ "--mcp-config",
994
+ spec.mcp_config_path,
995
+ "--strict-mcp-config",
996
+ "--allowed-tools",
997
+ "mcp__termhub__*",
998
+ "--disallowed-tools",
999
+ DISALLOWED_TOOLS,
1000
+ ...spec.model ? ["--model", spec.model] : []
1001
+ ];
1002
+ }
1003
+ function mcpConfig(url, token) {
1004
+ return JSON.stringify({ mcpServers: { termhub: { type: "http", url, headers: { Authorization: `Bearer ${token}` } } } });
1005
+ }
1006
+ function classifyFailure(stderr) {
1007
+ if (/No conversation found/i.test(stderr))
1008
+ return "missing_session";
1009
+ if (/^Error: --/m.test(stderr))
1010
+ return "cli_rejected";
1011
+ return "run_failed";
1012
+ }
1013
+
1014
+ // src/claude/run.ts
1015
+ import { spawn } from "child_process";
1016
+ import { mkdtempSync, rmSync, writeFileSync } from "fs";
1017
+ import { tmpdir } from "os";
1018
+ import { join } from "path";
1019
+ var DEFAULT_TIMEOUT_MS2 = 10 * 60 * 1e3;
1020
+ var KILL_GRACE_MS = 2e3;
1021
+ var PROMPT_TIMEOUT_MS = 3e4;
1022
+ var MAX_LINE_BYTES = MAX_FRAME - HEADER_BYTES - 1;
1023
+ var STDERR_TAIL_BYTES = 4e3;
1024
+ var CLI = "claude";
1025
+ function message(err) {
1026
+ return err instanceof Error ? err.message : String(err);
1027
+ }
1028
+ function createClaudeManager(deps) {
1029
+ const runs = /* @__PURE__ */ new Map();
1030
+ return {
1031
+ // Synchronous from end to end (the promise is for symmetry with the PTY manager), so no
1032
+ // `close` for this channel can interleave before the run is in `runs` and killable.
1033
+ async open(ch, params, socket) {
1034
+ if (runs.has(ch)) {
1035
+ socket.sendControl({ type: "open_error", ch, error: { code: "invalid", message: "channel in use" } });
1036
+ return;
1037
+ }
1038
+ let dir;
1039
+ let mcpConfigPath;
1040
+ try {
1041
+ dir = mkdtempSync(join(deps.tmpDir ?? tmpdir(), "termhub-claude-"));
1042
+ mcpConfigPath = join(dir, "termhub-mcp.json");
1043
+ writeFileSync(mcpConfigPath, mcpConfig(params.mcp_url, params.token), { mode: 384 });
1044
+ } catch (err) {
1045
+ deps.log("claude run could not prepare its mcp config", { ch, error: message(err) });
1046
+ if (dir) rmSync(dir, { recursive: true, force: true });
1047
+ socket.sendControl({ type: "open_error", ch, error: { code: "internal", message: "failed to prepare the claude run" } });
1048
+ return;
1049
+ }
1050
+ const runDir = dir;
1051
+ socket.sendControl({ type: "opened", ch });
1052
+ deps.log("claude run starting", { ch, resume: params.resume, model: params.model ?? null });
1053
+ const env = { ...deps.env ?? agentEnv() };
1054
+ if (params.config_dir === null) delete env.CLAUDE_CONFIG_DIR;
1055
+ else env.CLAUDE_CONFIG_DIR = params.config_dir;
1056
+ const args = buildClaudeArgs({
1057
+ session_id: params.session_id,
1058
+ resume: params.resume,
1059
+ mcp_config_path: mcpConfigPath,
1060
+ model: params.model ?? null
1061
+ });
1062
+ let child;
1063
+ try {
1064
+ child = spawn(CLI, args, { env, stdio: ["pipe", "pipe", "pipe"], detached: true });
1065
+ } catch (err) {
1066
+ deps.log("claude run could not be started", { ch, cli: CLI, error: message(err) });
1067
+ rmSync(runDir, { recursive: true, force: true });
1068
+ socket.sendControl({ type: "closed", ch, code: null, reason: "run_failed" });
1069
+ return;
1070
+ }
1071
+ let settled = false;
1072
+ let escalation;
1073
+ let stderr = "";
1074
+ let stderrBytes = 0;
1075
+ let buffer = "";
1076
+ let overlong = false;
1077
+ let droppedLines = 0;
1078
+ function signalRun(signal) {
1079
+ try {
1080
+ if (child.pid) process.kill(-child.pid, signal);
1081
+ else child.kill(signal);
1082
+ } catch {
1083
+ try {
1084
+ child.kill(signal);
1085
+ } catch {
1086
+ }
1087
+ }
1088
+ }
1089
+ function killRun(hard = false) {
1090
+ if (child.exitCode !== null || child.signalCode !== null) return;
1091
+ signalRun("SIGTERM");
1092
+ if (hard) {
1093
+ signalRun("SIGKILL");
1094
+ return;
1095
+ }
1096
+ if (escalation) return;
1097
+ escalation = setTimeout(() => signalRun("SIGKILL"), KILL_GRACE_MS);
1098
+ escalation.unref();
1099
+ }
1100
+ function settle(code, reason, notify = true) {
1101
+ if (settled) return;
1102
+ settled = true;
1103
+ clearTimeout(timer);
1104
+ clearTimeout(promptTimer);
1105
+ if (runs.get(ch) === run2) runs.delete(ch);
1106
+ try {
1107
+ rmSync(runDir, { recursive: true, force: true });
1108
+ } catch (err) {
1109
+ deps.log("claude run dir could not be removed", { ch, error: message(err) });
1110
+ }
1111
+ if (!notify) return;
1112
+ try {
1113
+ socket.sendControl(reason ? { type: "closed", ch, code, reason } : { type: "closed", ch, code });
1114
+ } catch (err) {
1115
+ deps.log("claude closed send failed", { ch, error: message(err) });
1116
+ }
1117
+ }
1118
+ function emitLine(line) {
1119
+ if (!line.trim()) return;
1120
+ if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
1121
+ droppedLines += 1;
1122
+ deps.log("claude stdout line too large to frame, dropped", { ch, bytes: Buffer.byteLength(line, "utf8") });
1123
+ return;
1124
+ }
1125
+ sendLine(line);
1126
+ }
1127
+ function sendLine(line) {
1128
+ if (settled) return;
1129
+ try {
1130
+ socket.sendStream(ch, Buffer.from(`${line}
1131
+ `, "utf8"));
1132
+ } catch (err) {
1133
+ deps.log("claude stream send failed", { ch, error: message(err) });
1134
+ }
1135
+ }
1136
+ const timer = setTimeout(() => {
1137
+ deps.log("claude run timed out", { ch, timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2 });
1138
+ killRun();
1139
+ }, deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
1140
+ timer.unref();
1141
+ const promptTimer = setTimeout(() => {
1142
+ deps.log("claude run got no prompt", { ch, promptTimeoutMs: deps.promptTimeoutMs ?? PROMPT_TIMEOUT_MS });
1143
+ killRun();
1144
+ settle(null, "run_failed");
1145
+ }, deps.promptTimeoutMs ?? PROMPT_TIMEOUT_MS);
1146
+ promptTimer.unref();
1147
+ const run2 = {
1148
+ child,
1149
+ promptSent: false,
1150
+ kill: killRun,
1151
+ promptArrived: () => clearTimeout(promptTimer),
1152
+ settle
1153
+ };
1154
+ runs.set(ch, run2);
1155
+ child.stdin?.on("error", () => {
1156
+ });
1157
+ child.stdout?.setEncoding("utf8");
1158
+ child.stdout?.on("data", (chunk) => {
1159
+ buffer += chunk;
1160
+ for (; ; ) {
1161
+ const nl = buffer.indexOf("\n");
1162
+ if (nl === -1) break;
1163
+ const line = buffer.slice(0, nl);
1164
+ buffer = buffer.slice(nl + 1);
1165
+ if (overlong) overlong = false;
1166
+ else emitLine(line);
1167
+ }
1168
+ if (Buffer.byteLength(buffer, "utf8") <= MAX_LINE_BYTES) return;
1169
+ if (!overlong) {
1170
+ overlong = true;
1171
+ droppedLines += 1;
1172
+ deps.log("claude stdout line too large to frame, dropped", { ch, bytes: Buffer.byteLength(buffer, "utf8") });
1173
+ }
1174
+ buffer = "";
1175
+ });
1176
+ child.stderr?.on("data", (data) => {
1177
+ stderrBytes += data.length;
1178
+ stderr = (stderr + data.toString("utf8")).slice(-STDERR_TAIL_BYTES);
1179
+ });
1180
+ child.on("error", (err) => {
1181
+ const missing = err.code === "ENOENT";
1182
+ deps.log(missing ? `claude cli not found on this machine (${CLI})` : "claude run failed to start", { ch, cli: CLI, error: err.message });
1183
+ killRun();
1184
+ settle(null, missing ? "cli_missing" : "run_failed");
1185
+ });
1186
+ child.on("close", (code) => {
1187
+ if (escalation) clearTimeout(escalation);
1188
+ if (!overlong) emitLine(buffer);
1189
+ buffer = "";
1190
+ deps.log("claude run ended", { ch, code, stderrBytes, droppedLines });
1191
+ if (code === 0) settle(0);
1192
+ else settle(code, classifyFailure(stderr));
1193
+ });
1194
+ },
1195
+ write(ch, data) {
1196
+ const run2 = runs.get(ch);
1197
+ if (!run2) return false;
1198
+ if (run2.promptSent) {
1199
+ deps.log("extra data on a claude channel ignored", { ch, bytes: data.length });
1200
+ return true;
1201
+ }
1202
+ run2.promptSent = true;
1203
+ run2.promptArrived();
1204
+ run2.child.stdin?.end(data);
1205
+ return true;
1206
+ },
1207
+ close(ch) {
1208
+ const run2 = runs.get(ch);
1209
+ if (!run2) return;
1210
+ run2.kill();
1211
+ run2.settle(null, "killed");
1212
+ },
1213
+ closeAll() {
1214
+ for (const run2 of [...runs.values()]) {
1215
+ run2.kill(true);
1216
+ run2.settle(null, "killed", false);
1217
+ }
1218
+ }
1219
+ };
1220
+ }
1221
+
949
1222
  // src/dispatch.ts
950
1223
  async function handleRpc(msg, socket, handlers2, log2) {
951
1224
  const method = msg.method;
@@ -986,16 +1259,19 @@ function createDispatcher(deps) {
986
1259
  case "rpc":
987
1260
  void handleRpc(msg, socket, deps.handlers, deps.log);
988
1261
  break;
989
- case "open":
990
- deps.pty.open(msg.ch, msg.params, socket).catch((err) => {
991
- deps.log("pty.open rejected unexpectedly", { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
1262
+ case "open": {
1263
+ const opened = msg.kind === "claude" ? deps.claude.open(msg.ch, msg.params, socket) : deps.pty.open(msg.ch, msg.params, socket);
1264
+ opened.catch((err) => {
1265
+ deps.log(`${msg.kind}.open rejected unexpectedly`, { ch: msg.ch, error: err instanceof Error ? err.message : String(err) });
992
1266
  });
993
1267
  break;
1268
+ }
994
1269
  case "resize":
995
1270
  deps.pty.resize(msg.ch, msg.cols, msg.rows);
996
1271
  break;
997
1272
  case "close":
998
1273
  deps.pty.close(msg.ch);
1274
+ deps.claude.close(msg.ch);
999
1275
  break;
1000
1276
  }
1001
1277
  };
@@ -1060,8 +1336,8 @@ function isSpawnHelperFailure(err) {
1060
1336
  function isEnoent2(err) {
1061
1337
  const e = err;
1062
1338
  if (e?.code === "ENOENT") return true;
1063
- const message = e instanceof Error ? e.message : String(err);
1064
- return /ENOENT/.test(message);
1339
+ const message2 = e instanceof Error ? e.message : String(err);
1340
+ return /ENOENT/.test(message2);
1065
1341
  }
1066
1342
  function createPtyManager(deps) {
1067
1343
  const procs = /* @__PURE__ */ new Map();
@@ -1092,8 +1368,8 @@ function createPtyManager(deps) {
1092
1368
  TERMHUB_TAB_ID: params.session,
1093
1369
  TERMHUB_SESSION: params.session
1094
1370
  };
1095
- const spawn = await resolveSpawn();
1096
- const spawnTmux = () => spawn(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
1371
+ const spawn2 = await resolveSpawn();
1372
+ const spawnTmux = () => spawn2(tmux, ["-u", "new-session", "-A", "-s", params.session, "-c", cwd], { name: "xterm-256color", cols, rows, cwd, env });
1097
1373
  try {
1098
1374
  proc = spawnTmux();
1099
1375
  } catch (err) {
@@ -1557,7 +1833,7 @@ async function status3() {
1557
1833
  }
1558
1834
 
1559
1835
  // src/version.ts
1560
- var AGENT_VERSION = "0.3.0";
1836
+ var AGENT_VERSION = "0.4.0";
1561
1837
 
1562
1838
  // src/rpc/update.ts
1563
1839
  var PACKAGE = "@termhub/agent";
@@ -1646,6 +1922,7 @@ function detectOs(platform = process.platform) {
1646
1922
  if (platform === "linux") return "linux";
1647
1923
  return null;
1648
1924
  }
1925
+ var CAPABILITIES = [CAPABILITY_CLAUDE];
1649
1926
  async function buildHello(osName) {
1650
1927
  let tools = [];
1651
1928
  try {
@@ -1660,7 +1937,8 @@ async function buildHello(osName) {
1660
1937
  arch: process.arch,
1661
1938
  hostname: os6.hostname(),
1662
1939
  tmux: tools.includes("tmux"),
1663
- tools
1940
+ tools,
1941
+ capabilities: CAPABILITIES
1664
1942
  };
1665
1943
  }
1666
1944
  var REVOKED_MESSAGE = "Token inv\xE1lido ou revogado";
@@ -1678,7 +1956,7 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
1678
1956
  {
1679
1957
  url: config.url,
1680
1958
  token: config.token,
1681
- hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], probe: true },
1959
+ hello: { agent_version: AGENT_VERSION, os: osName, arch: process.arch, hostname: os6.hostname(), tmux: false, tools: [], capabilities: CAPABILITIES, probe: true },
1682
1960
  onServerMessage: () => {
1683
1961
  },
1684
1962
  onStream: () => {
@@ -1704,8 +1982,8 @@ async function checkServerConnection(config, timeoutMs = 5e3) {
1704
1982
  controller.abort();
1705
1983
  }
1706
1984
  }
1707
- async function exitWithoutRestart(message) {
1708
- console.error(message);
1985
+ async function exitWithoutRestart(message2) {
1986
+ console.error(message2);
1709
1987
  await stopRestartLoop();
1710
1988
  process.exit(78);
1711
1989
  }
@@ -1720,7 +1998,8 @@ async function runAgent(config, opts) {
1720
1998
  if (helper.repaired) opts.log("spawn-helper exec bit repaired", { path: helper.path });
1721
1999
  else if (!helper.executable) opts.log("spawn-helper is not executable and could not be fixed", { path: helper.path, error: helper.error });
1722
2000
  const pty = createPtyManager({ log: opts.log });
1723
- const dispatch = createDispatcher({ handlers, pty, log: opts.log });
2001
+ const claude = createClaudeManager({ log: opts.log });
2002
+ const dispatch = createDispatcher({ handlers, pty, claude, log: opts.log });
1724
2003
  const healHooks = () => {
1725
2004
  heal().then((dirs) => {
1726
2005
  if (dirs.length) opts.log("monitor hooks repaired", { dirs: dirs.length });
@@ -1734,9 +2013,16 @@ async function runAgent(config, opts) {
1734
2013
  token: config.token,
1735
2014
  hello,
1736
2015
  onServerMessage: dispatch,
1737
- onStream: (ch, data) => pty.write(ch, data),
2016
+ // A frame belongs to whichever manager holds that channel: the claude one says so, and
2017
+ // anything it does not own is a terminal's.
2018
+ onStream: (ch, data) => {
2019
+ if (!claude.write(ch, data)) pty.write(ch, data);
2020
+ },
1738
2021
  onConnect: healHooks,
1739
- onDisconnect: () => pty.closeAll(),
2022
+ onDisconnect: () => {
2023
+ pty.closeAll();
2024
+ claude.closeAll();
2025
+ },
1740
2026
  log: opts.log
1741
2027
  },
1742
2028
  opts.signal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@termhub/agent",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Agente do termhub: conecta esta máquina ao servidor por WebSocket de saída (sem SSH) e expõe os terminais tmux no navegador.",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -34,6 +34,7 @@
34
34
  },
35
35
  "devDependencies": {
36
36
  "@termhub/agent-protocol": "*",
37
+ "@termhub/claude-cli": "*",
37
38
  "@termhub/machine-ops": "*",
38
39
  "@types/node": "^22.10.5",
39
40
  "@types/ws": "^8.5.13",