@opentiny/webmcp-cli 0.1.0 → 0.1.1

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/dist/bin.cjs CHANGED
@@ -548,9 +548,17 @@ var init_package = __esm({
548
548
  "package.json"() {
549
549
  package_default = {
550
550
  name: "@opentiny/webmcp-cli",
551
- version: "0.1.0",
551
+ version: "0.1.1",
552
552
  type: "module",
553
553
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
554
+ repository: {
555
+ type: "git",
556
+ url: "git+https://github.com/opentiny/webmcp-sdk.git",
557
+ directory: "packages/webmcp-cli"
558
+ },
559
+ bugs: {
560
+ url: "https://github.com/opentiny/webmcp-sdk/issues"
561
+ },
554
562
  main: "dist/index.js",
555
563
  types: "dist/index.d.ts",
556
564
  bin: {
@@ -558,7 +566,8 @@ var init_package = __esm({
558
566
  },
559
567
  files: [
560
568
  "dist",
561
- "README.md"
569
+ "README.md",
570
+ "scripts/ensure-pty-helper.mjs"
562
571
  ],
563
572
  scripts: {
564
573
  dev: "tsup --watch",
@@ -567,6 +576,7 @@ var init_package = __esm({
567
576
  prepublishOnly: "pnpm run build",
568
577
  "link:global": "npm install -g .",
569
578
  start: "node dist/bin.js",
579
+ postinstall: "node scripts/ensure-pty-helper.mjs",
570
580
  test: "vitest run",
571
581
  "test:watch": "vitest",
572
582
  "test:browser": "vitest run --config vitest.e2e.config.ts"
@@ -579,7 +589,7 @@ var init_package = __esm({
579
589
  license: "MIT",
580
590
  author: "OpenTiny Team",
581
591
  engines: {
582
- node: ">=16"
592
+ node: ">=18.19.0"
583
593
  },
584
594
  publishConfig: {
585
595
  access: "public"
@@ -591,6 +601,7 @@ var init_package = __esm({
591
601
  "@page-agent/page-controller": "catalog:",
592
602
  clipboardy: "^5.3.1",
593
603
  commander: "^11.1.0",
604
+ execa: "^9.6.1",
594
605
  json5: "^2.2.3",
595
606
  marked: "^15.0.7",
596
607
  picocolors: "^1.0.0",
@@ -598,6 +609,9 @@ var init_package = __esm({
598
609
  ws: "^8.18.0",
599
610
  zod: "^3.23.0"
600
611
  },
612
+ optionalDependencies: {
613
+ "node-pty": "^1.1.0"
614
+ },
601
615
  devDependencies: {
602
616
  "@types/node": "^20.0.0",
603
617
  "@types/ws": "^8.5.10",
@@ -819,12 +833,15 @@ var init_cdp_adapter = __esm({
819
833
  });
820
834
 
821
835
  // src/bridge/protocol.ts
822
- var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS;
836
+ function isTerminalMethod(method) {
837
+ return TERMINAL_METHOD_SET.has(method);
838
+ }
839
+ var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS, TERMINAL_EVENTS, BASH_EVENTS, TERMINAL_METHOD_SET;
823
840
  var init_protocol = __esm({
824
841
  "src/bridge/protocol.ts"() {
825
842
  "use strict";
826
843
  DEFAULT_WS_PORT = 18999;
827
- PROTOCOL_VERSION = "1.1.0";
844
+ PROTOCOL_VERSION = "1.2.0";
828
845
  WS_CLOSE_CODE = {
829
846
  /** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
830
847
  REPLACED: 4e3,
@@ -858,8 +875,25 @@ var init_protocol = __esm({
858
875
  SKILLS_GET: "skills/get",
859
876
  SKILLS_READ_RESOURCE: "skills/read_resource",
860
877
  SYSTEM_BASH: "system/bash",
861
- SYSTEM_PICK_DIRECTORY: "system/pick_directory"
878
+ SYSTEM_PICK_DIRECTORY: "system/pick_directory",
879
+ TERMINAL_OPEN: "terminal/open",
880
+ TERMINAL_INPUT: "terminal/input",
881
+ TERMINAL_RESIZE: "terminal/resize",
882
+ TERMINAL_CLOSE: "terminal/close"
883
+ };
884
+ TERMINAL_EVENTS = {
885
+ OUTPUT: "notification/terminal_output",
886
+ EXIT: "notification/terminal_exit"
862
887
  };
888
+ BASH_EVENTS = {
889
+ OUTPUT: "notification/bash_output"
890
+ };
891
+ TERMINAL_METHOD_SET = /* @__PURE__ */ new Set([
892
+ BRIDGE_METHODS.TERMINAL_OPEN,
893
+ BRIDGE_METHODS.TERMINAL_INPUT,
894
+ BRIDGE_METHODS.TERMINAL_RESIZE,
895
+ BRIDGE_METHODS.TERMINAL_CLOSE
896
+ ]);
863
897
  }
864
898
  });
865
899
 
@@ -968,26 +1002,137 @@ function resolveShell(forcePlatform) {
968
1002
  if (!forcePlatform) cachedShell = fallback;
969
1003
  return fallback;
970
1004
  }
971
- function killProcessTree(child) {
972
- if (process.platform === "win32" && child.pid) {
1005
+ function stripAnsi(text) {
1006
+ return text.replace(ANSI_PATTERN, "");
1007
+ }
1008
+ function createAnsiStripper() {
1009
+ let pending = "";
1010
+ const take = (final) => {
1011
+ let out = "";
1012
+ let index = 0;
1013
+ while (index < pending.length) {
1014
+ if (pending[index] !== "\x1B") {
1015
+ out += pending[index];
1016
+ index += 1;
1017
+ continue;
1018
+ }
1019
+ const rest = pending.slice(index);
1020
+ ANSI_PATTERN.lastIndex = 0;
1021
+ const match = ANSI_PATTERN.exec(rest);
1022
+ if (match && match.index === 0) {
1023
+ index += match[0].length;
1024
+ continue;
1025
+ }
1026
+ if (!final && rest.length < 128 && isOpenAnsi(rest)) {
1027
+ pending = rest;
1028
+ return out;
1029
+ }
1030
+ out += pending[index];
1031
+ index += 1;
1032
+ }
1033
+ pending = "";
1034
+ return out;
1035
+ };
1036
+ return {
1037
+ push(text) {
1038
+ pending += text;
1039
+ return take(false);
1040
+ },
1041
+ end() {
1042
+ return take(true);
1043
+ }
1044
+ };
1045
+ }
1046
+ function isOpenAnsi(text) {
1047
+ if (!text.startsWith("\x1B")) return false;
1048
+ if (text.length === 1) return true;
1049
+ if (text[1] === "[") {
1050
+ let index = 2;
1051
+ while (index < text.length && /[0-?]/.test(text[index])) index += 1;
1052
+ while (index < text.length && /[ -/]/.test(text[index])) index += 1;
1053
+ return index >= text.length;
1054
+ }
1055
+ if (text[1] === "]") return !text.includes("\x07") && !text.includes("\x1B\\");
1056
+ return false;
1057
+ }
1058
+ function withoutCarriageReturn(buf) {
1059
+ if (!buf.includes(13)) return buf;
1060
+ const out = Buffer.allocUnsafe(buf.length);
1061
+ let size = 0;
1062
+ for (let index = 0; index < buf.length; index += 1) {
1063
+ const byte = buf[index];
1064
+ if (byte === 13) continue;
1065
+ out[size] = byte;
1066
+ size += 1;
1067
+ }
1068
+ return out.subarray(0, size);
1069
+ }
1070
+ function collectLimited(stream, maxBuffer, onText, stripCarriageReturn = false, ansi) {
1071
+ const state = { chunks: [], truncated: false };
1072
+ if (!stream) return state;
1073
+ const decoder = new import_node_string_decoder.StringDecoder("utf8");
1074
+ let bytes = 0;
1075
+ const emitText = (text) => {
1076
+ const cleaned = ansi ? ansi.push(text) : text;
1077
+ if (cleaned) onText?.(cleaned);
1078
+ };
1079
+ const emit = (buf) => {
1080
+ emitText(decoder.write(buf));
1081
+ };
1082
+ stream.on("data", (chunk) => {
1083
+ const raw = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1084
+ const buf = stripCarriageReturn ? withoutCarriageReturn(raw) : raw;
1085
+ if (buf.length === 0) return;
1086
+ emit(buf);
1087
+ if (bytes >= maxBuffer) {
1088
+ state.truncated = true;
1089
+ return;
1090
+ }
1091
+ const remaining = maxBuffer - bytes;
1092
+ if (buf.length <= remaining) {
1093
+ state.chunks.push(buf);
1094
+ bytes += buf.length;
1095
+ return;
1096
+ }
1097
+ state.chunks.push(buf.subarray(0, remaining));
1098
+ bytes += remaining;
1099
+ state.truncated = true;
1100
+ });
1101
+ stream.on("end", () => {
1102
+ const rest = ansi ? `${ansi.push(decoder.end())}${ansi.end()}` : decoder.end();
1103
+ if (rest) onText?.(rest);
1104
+ });
1105
+ return state;
1106
+ }
1107
+ function decodeLimited(state, removeAnsi = false) {
1108
+ const decoder = new import_node_string_decoder.StringDecoder("utf8");
1109
+ const text = decoder.write(Buffer.concat(state.chunks));
1110
+ const flushed = state.truncated ? "" : decoder.end();
1111
+ const decoded = text + flushed;
1112
+ return (removeAnsi ? stripAnsi(decoded) : decoded).trim();
1113
+ }
1114
+ function signalProcessTree(pid, signal) {
1115
+ if (!pid || pid <= 0) return;
1116
+ if (process.platform === "win32") {
973
1117
  try {
974
- (0, import_node_child_process.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
975
- windowsHide: true,
976
- stdio: "ignore"
977
- });
1118
+ (0, import_node_child_process.spawnSync)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, timeout: 5e3 });
1119
+ } catch {
1120
+ }
1121
+ return;
1122
+ }
1123
+ try {
1124
+ process.kill(-pid, signal);
1125
+ } catch {
1126
+ try {
1127
+ process.kill(pid, signal);
978
1128
  } catch {
979
- child.kill("SIGKILL");
980
1129
  }
981
- } else {
982
- child.kill("SIGTERM");
983
- setTimeout(() => {
984
- try {
985
- child.kill("SIGKILL");
986
- } catch {
987
- }
988
- }, 2e3);
989
1130
  }
990
1131
  }
1132
+ function ignoreStreamError(stream) {
1133
+ stream?.on("error", () => {
1134
+ });
1135
+ }
991
1136
  async function executeBash(params) {
992
1137
  const trimmedCommand = params.command?.trim();
993
1138
  if (!trimmedCommand) {
@@ -1005,83 +1150,91 @@ async function executeBash(params) {
1005
1150
  });
1006
1151
  const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
1007
1152
  const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
1008
- const startTime = Date.now();
1009
1153
  const shell = resolveShell();
1010
- return new Promise((resolve3, reject) => {
1011
- const stdoutChunks = [];
1012
- let stdoutBytes = 0;
1013
- const stderrChunks = [];
1014
- let stderrBytes = 0;
1015
- let truncated = false;
1016
- let isKilled = false;
1017
- const child = (0, import_node_child_process.spawn)(trimmedCommand, {
1018
- shell,
1019
- cwd,
1020
- env: { ...process.env, ...env },
1021
- windowsHide: true
1022
- });
1023
- const killTimeoutTimer = setTimeout(() => {
1024
- isKilled = true;
1025
- killProcessTree(child);
1026
- reject(new BashTimeoutError(normalizedTimeoutMs));
1027
- }, normalizedTimeoutMs);
1028
- child.stdout.on("data", (chunk) => {
1029
- if (stdoutBytes < maxBuffer) {
1030
- const remaining = maxBuffer - stdoutBytes;
1031
- if (chunk.length <= remaining) {
1032
- stdoutChunks.push(chunk);
1033
- stdoutBytes += chunk.length;
1034
- } else {
1035
- stdoutChunks.push(chunk.subarray(0, remaining));
1036
- stdoutBytes += remaining;
1037
- truncated = true;
1038
- }
1039
- } else {
1040
- truncated = true;
1041
- }
1042
- });
1043
- child.stderr.on("data", (chunk) => {
1044
- if (stderrBytes < maxBuffer) {
1045
- const remaining = maxBuffer - stderrBytes;
1046
- if (chunk.length <= remaining) {
1047
- stderrChunks.push(chunk);
1048
- stderrBytes += chunk.length;
1049
- } else {
1050
- stderrChunks.push(chunk.subarray(0, remaining));
1051
- stderrBytes += remaining;
1052
- truncated = true;
1053
- }
1054
- } else {
1055
- truncated = true;
1056
- }
1057
- });
1058
- child.on("error", (err) => {
1059
- clearTimeout(killTimeoutTimer);
1060
- reject(err);
1061
- });
1062
- child.on("close", (exitCode, signal) => {
1063
- clearTimeout(killTimeoutTimer);
1064
- if (isKilled) return;
1065
- const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
1066
- const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
1067
- resolve3({
1068
- stdout,
1069
- stderr,
1070
- exitCode,
1071
- signal,
1072
- durationMs: Date.now() - startTime,
1073
- ...truncated ? { truncated: true } : {}
1074
- });
1075
- });
1154
+ const useTty = params.tty === true && process.platform !== "win32";
1155
+ const detached = process.platform !== "win32";
1156
+ const subprocess = useTty ? (0, import_execa.execa)("python3", ["-u", "-c", TTY_HELPER], {
1157
+ cwd,
1158
+ env: {
1159
+ ...env,
1160
+ PAGER: "cat",
1161
+ GIT_PAGER: "cat",
1162
+ BASH_TTY_COMMAND: trimmedCommand,
1163
+ BASH_TTY_SHELL: shell
1164
+ },
1165
+ stdin: "ignore",
1166
+ buffer: false,
1167
+ encoding: "buffer",
1168
+ reject: false,
1169
+ windowsHide: true,
1170
+ stripFinalNewline: false,
1171
+ detached
1172
+ }) : (0, import_execa.execa)(trimmedCommand, {
1173
+ shell,
1174
+ cwd,
1175
+ env,
1176
+ stdin: "ignore",
1177
+ buffer: false,
1178
+ encoding: "buffer",
1179
+ reject: false,
1180
+ windowsHide: true,
1181
+ stripFinalNewline: false,
1182
+ detached
1076
1183
  });
1184
+ ignoreStreamError(subprocess.stdout);
1185
+ ignoreStreamError(subprocess.stderr);
1186
+ let timedOut = false;
1187
+ let forceTimer;
1188
+ const timer = setTimeout(() => {
1189
+ timedOut = true;
1190
+ signalProcessTree(subprocess.pid, "SIGTERM");
1191
+ forceTimer = setTimeout(() => {
1192
+ signalProcessTree(subprocess.pid, "SIGKILL");
1193
+ subprocess.stdout?.destroy();
1194
+ subprocess.stderr?.destroy();
1195
+ }, FORCE_KILL_AFTER_DELAY_MS);
1196
+ }, normalizedTimeoutMs);
1197
+ const emit = (stream) => (text) => {
1198
+ params.onOutput?.({ stream, text });
1199
+ };
1200
+ const ansi = useTty ? createAnsiStripper() : void 0;
1201
+ const stdoutState = collectLimited(subprocess.stdout, maxBuffer, emit("stdout"), useTty, ansi);
1202
+ const stderrState = collectLimited(subprocess.stderr, maxBuffer, emit("stderr"));
1203
+ let result;
1204
+ try {
1205
+ result = await subprocess;
1206
+ } catch (err) {
1207
+ if (timedOut) throw new BashTimeoutError(normalizedTimeoutMs);
1208
+ throw err;
1209
+ } finally {
1210
+ clearTimeout(timer);
1211
+ if (forceTimer) clearTimeout(forceTimer);
1212
+ }
1213
+ if (timedOut) {
1214
+ throw new BashTimeoutError(normalizedTimeoutMs);
1215
+ }
1216
+ if (result.failed && result.exitCode == null && result.signal == null && !result.isTerminated) {
1217
+ throw new Error(result.shortMessage || result.message || "\u547D\u4EE4\u542F\u52A8\u5931\u8D25");
1218
+ }
1219
+ const truncated = stdoutState.truncated || stderrState.truncated;
1220
+ return {
1221
+ stdout: decodeLimited(stdoutState, useTty),
1222
+ stderr: decodeLimited(stderrState),
1223
+ exitCode: result.exitCode ?? null,
1224
+ signal: result.signal ?? null,
1225
+ durationMs: result.durationMs ?? 0,
1226
+ ...truncated ? { truncated: true } : {}
1227
+ };
1077
1228
  }
1078
- var import_node_fs2, import_node_path2, import_node_child_process, BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell;
1229
+ var import_node_child_process, import_node_fs2, import_node_path2, import_node_string_decoder, import_execa, BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell, FORCE_KILL_AFTER_DELAY_MS, TTY_HELPER, ANSI_PATTERN;
1079
1230
  var init_bash_executor = __esm({
1080
1231
  "src/bridge/bash-executor.ts"() {
1081
1232
  "use strict";
1233
+ import_node_child_process = require("child_process");
1082
1234
  import_node_fs2 = __toESM(require("fs"), 1);
1083
1235
  import_node_path2 = __toESM(require("path"), 1);
1084
- import_node_child_process = require("child_process");
1236
+ import_node_string_decoder = require("string_decoder");
1237
+ import_execa = require("execa");
1085
1238
  init_workspace_manager();
1086
1239
  BashTimeoutError = class extends Error {
1087
1240
  code = "TIMEOUT";
@@ -1096,6 +1249,51 @@ var init_bash_executor = __esm({
1096
1249
  MAX_TIMEOUT_MS = 6e5;
1097
1250
  DEFAULT_MAX_BUFFER = 512 * 1024;
1098
1251
  cachedShell = null;
1252
+ FORCE_KILL_AFTER_DELAY_MS = 2e3;
1253
+ TTY_HELPER = `
1254
+ import errno, os, sys
1255
+ cmd = os.environ.get("BASH_TTY_COMMAND") or ""
1256
+ shell = os.environ.get("BASH_TTY_SHELL") or "/bin/bash"
1257
+ if not cmd:
1258
+ sys.exit(127)
1259
+ import pty
1260
+ pid, fd = pty.fork()
1261
+ if pid == 0:
1262
+ os.environ.pop("BASH_TTY_COMMAND", None)
1263
+ os.environ.pop("BASH_TTY_SHELL", None)
1264
+ os.environ["TERM"] = "xterm-256color"
1265
+ try:
1266
+ # stdout/stderr \u4ECD\u662F\u7EC8\u7AEF\uFF1Bstdin \u6362\u6210 /dev/null\uFF0C\u8BFB\u53D6\u8F93\u5165\u7684\u547D\u4EE4\u4F1A\u7ACB\u523B\u9047\u5230 EOF
1267
+ null_fd = os.open(os.devnull, os.O_RDONLY)
1268
+ os.dup2(null_fd, 0)
1269
+ os.close(null_fd)
1270
+ os.execvp(shell, [shell, "-c", cmd])
1271
+ except OSError:
1272
+ os._exit(127)
1273
+ while True:
1274
+ try:
1275
+ data = os.read(fd, 65536)
1276
+ except OSError as exc:
1277
+ if exc.errno == errno.EINTR:
1278
+ continue
1279
+ break
1280
+ if not data:
1281
+ break
1282
+ try:
1283
+ os.write(1, data)
1284
+ except OSError:
1285
+ break
1286
+ try:
1287
+ _, status = os.waitpid(pid, 0)
1288
+ except OSError:
1289
+ sys.exit(1)
1290
+ if os.WIFEXITED(status):
1291
+ sys.exit(os.WEXITSTATUS(status))
1292
+ if os.WIFSIGNALED(status):
1293
+ sys.exit(128 + os.WTERMSIG(status))
1294
+ sys.exit(1)
1295
+ `;
1296
+ ANSI_PATTERN = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][^\u0007]*(?:\u0007|\u001B\\)|[@-Z\\-_])/g;
1099
1297
  }
1100
1298
  });
1101
1299
 
@@ -1307,6 +1505,387 @@ var init_directory_picker = __esm({
1307
1505
  }
1308
1506
  });
1309
1507
 
1508
+ // src/bridge/pty-backend.ts
1509
+ function interactiveArgs(shell) {
1510
+ if (process.platform !== "win32") return ["-i"];
1511
+ const base = import_node_path3.default.basename(shell).toLowerCase();
1512
+ if (base === "powershell.exe" || base === "pwsh.exe") return ["-NoLogo"];
1513
+ if (base === "cmd.exe") return [];
1514
+ return ["-i"];
1515
+ }
1516
+ function ptyEnvironment() {
1517
+ const env = {};
1518
+ for (const [key, value] of Object.entries(process.env)) {
1519
+ if (value !== void 0) env[key] = value;
1520
+ }
1521
+ env.TERM = "xterm-256color";
1522
+ return env;
1523
+ }
1524
+ function resolveSpawnFile(shell, pathEnv = process.env.PATH || "") {
1525
+ if (!shell || import_node_path3.default.isAbsolute(shell)) return shell;
1526
+ const names = process.platform === "win32" && !shell.toLowerCase().endsWith(".exe") ? [shell, `${shell}.exe`] : [shell];
1527
+ for (const dir of pathEnv.split(import_node_path3.default.delimiter)) {
1528
+ if (!dir) continue;
1529
+ for (const name of names) {
1530
+ const candidate = import_node_path3.default.join(dir, name);
1531
+ try {
1532
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
1533
+ } catch {
1534
+ }
1535
+ }
1536
+ }
1537
+ if (process.platform === "win32") {
1538
+ const systemRoot = process.env.SystemRoot || "C:\\Windows";
1539
+ const fallbacks = [
1540
+ import_node_path3.default.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
1541
+ import_node_path3.default.join(systemRoot, "Sysnative", "WindowsPowerShell", "v1.0", "powershell.exe")
1542
+ ];
1543
+ for (const candidate of fallbacks) {
1544
+ try {
1545
+ if ((0, import_node_fs3.existsSync)(candidate)) return candidate;
1546
+ } catch {
1547
+ }
1548
+ }
1549
+ }
1550
+ return shell;
1551
+ }
1552
+ function describeSpawnHelperChmodError(helper, code) {
1553
+ if (code !== "EPERM" && code !== "EACCES") return null;
1554
+ return `spawn-helper \u65E0\u6CD5\u8BBE\u7F6E\u4E3A\u53EF\u6267\u884C\uFF08${helper}\uFF09\u3002\u5F53\u524D\u7528\u6237\u6CA1\u6709\u6743\u9650\u4FEE\u6539\u8BE5\u6587\u4EF6\u3002\u8BF7\u7528\u5B89\u88C5\u65F6\u7684\u7528\u6237\u91CD\u65B0\u5B89\u88C5 @opentiny/webmcp-cli\uFF0C\u6216\u5BF9\u8BE5\u6587\u4EF6\u6267\u884C chmod 755\u3002`;
1555
+ }
1556
+ function loadNodePty() {
1557
+ try {
1558
+ return (0, import_node_module.createRequire)(import_meta2.url)("node-pty");
1559
+ } catch (err) {
1560
+ const detail = err instanceof Error ? err.message : String(err);
1561
+ throw new Error(`\u4EA4\u4E92\u7EC8\u7AEF\u4E0D\u53EF\u7528\uFF1A\u672A\u80FD\u52A0\u8F7D node-pty\uFF08${detail}\uFF09\u3002\u5176\u4F59\u6865\u63A5\u547D\u4EE4\u4ECD\u53EF\u4F7F\u7528\u3002`);
1562
+ }
1563
+ }
1564
+ function spawnHelperPath() {
1565
+ try {
1566
+ const pkgJson = (0, import_node_module.createRequire)(import_meta2.url).resolve("node-pty/package.json");
1567
+ return import_node_path3.default.join(import_node_path3.default.dirname(pkgJson), "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper");
1568
+ } catch {
1569
+ return null;
1570
+ }
1571
+ }
1572
+ function ensureSpawnHelper() {
1573
+ if (process.platform === "win32") return null;
1574
+ const helper = spawnHelperPath();
1575
+ if (!helper || !(0, import_node_fs3.existsSync)(helper)) return null;
1576
+ try {
1577
+ (0, import_node_fs3.chmodSync)(helper, 493);
1578
+ return null;
1579
+ } catch (err) {
1580
+ const code = err && typeof err === "object" && "code" in err ? String(err.code) : void 0;
1581
+ return describeSpawnHelperChmodError(helper, code);
1582
+ }
1583
+ }
1584
+ function signalName(code) {
1585
+ if (!code) return null;
1586
+ for (const [name, value] of Object.entries(import_node_os2.constants.signals)) {
1587
+ if (value === code) return name;
1588
+ }
1589
+ return String(code);
1590
+ }
1591
+ function descendantPids(rootPid) {
1592
+ const result = (0, import_node_child_process3.spawnSync)("ps", ["-ax", "-o", "pid=,ppid="], { encoding: "utf8", timeout: 3e3 });
1593
+ if (result.status !== 0 || !result.stdout) return [];
1594
+ const children = /* @__PURE__ */ new Map();
1595
+ for (const line of result.stdout.split("\n")) {
1596
+ const [pidText, ppidText] = line.trim().split(/\s+/);
1597
+ const pid = Number(pidText);
1598
+ const ppid = Number(ppidText);
1599
+ if (!pid || !ppid) continue;
1600
+ const list = children.get(ppid);
1601
+ if (list) list.push(pid);
1602
+ else children.set(ppid, [pid]);
1603
+ }
1604
+ const out = [];
1605
+ const walk = (pid) => {
1606
+ for (const child of children.get(pid) || []) {
1607
+ walk(child);
1608
+ out.push(child);
1609
+ }
1610
+ };
1611
+ walk(rootPid);
1612
+ return out;
1613
+ }
1614
+ function killPtySession(term) {
1615
+ const pid = term.pid;
1616
+ if (pid && pid > 0) {
1617
+ if (process.platform === "win32") {
1618
+ try {
1619
+ (0, import_node_child_process3.spawnSync)("taskkill", ["/pid", String(pid), "/T", "/F"], { windowsHide: true, timeout: 3e3 });
1620
+ } catch {
1621
+ }
1622
+ } else {
1623
+ for (const victim of [...descendantPids(pid), pid]) {
1624
+ try {
1625
+ process.kill(victim, "SIGKILL");
1626
+ } catch {
1627
+ }
1628
+ }
1629
+ }
1630
+ }
1631
+ try {
1632
+ term.kill("SIGHUP");
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ function spawnHostPty(options) {
1637
+ const pty = loadNodePty();
1638
+ const helperError = ensureSpawnHelper();
1639
+ const shell = resolveSpawnFile(options.shell || resolveShell());
1640
+ const pending = [];
1641
+ let dataCb = null;
1642
+ let exitCb = null;
1643
+ let pendingExit = null;
1644
+ let settled = false;
1645
+ let term = null;
1646
+ const emitData = (data) => {
1647
+ if (!data) return;
1648
+ if (dataCb) dataCb(data);
1649
+ else pending.push(data);
1650
+ };
1651
+ const finish = (info) => {
1652
+ if (settled) return;
1653
+ settled = true;
1654
+ if (exitCb) exitCb(info);
1655
+ else pendingExit = info;
1656
+ };
1657
+ try {
1658
+ term = pty.spawn(shell, interactiveArgs(shell), {
1659
+ name: "xterm-256color",
1660
+ cols: options.cols,
1661
+ rows: options.rows,
1662
+ cwd: options.cwd,
1663
+ env: ptyEnvironment()
1664
+ });
1665
+ term.onData((data) => emitData(data));
1666
+ term.onExit((event) => {
1667
+ finish({ exitCode: event.exitCode, signal: signalName(event.signal) });
1668
+ });
1669
+ } catch (err) {
1670
+ const message = err instanceof Error ? err.message : String(err);
1671
+ const detail = helperError ? `${helperError} ${message}` : message;
1672
+ setImmediate(() => {
1673
+ emitData(`\r
1674
+ [\u7EC8\u7AEF\u542F\u52A8\u5931\u8D25] ${detail}\r
1675
+ `);
1676
+ finish({ exitCode: null, signal: null });
1677
+ });
1678
+ }
1679
+ return {
1680
+ write(data) {
1681
+ try {
1682
+ term?.write(data);
1683
+ } catch {
1684
+ }
1685
+ },
1686
+ resize(cols, rows) {
1687
+ try {
1688
+ term?.resize(cols, rows);
1689
+ } catch {
1690
+ }
1691
+ },
1692
+ kill() {
1693
+ if (term) killPtySession(term);
1694
+ },
1695
+ pause() {
1696
+ try {
1697
+ term?.pause();
1698
+ } catch {
1699
+ }
1700
+ },
1701
+ resume() {
1702
+ try {
1703
+ term?.resume();
1704
+ } catch {
1705
+ }
1706
+ },
1707
+ onData(cb) {
1708
+ dataCb = cb;
1709
+ for (const chunk of pending) cb(chunk);
1710
+ pending.length = 0;
1711
+ },
1712
+ onExit(cb) {
1713
+ exitCb = cb;
1714
+ if (pendingExit) cb(pendingExit);
1715
+ }
1716
+ };
1717
+ }
1718
+ var import_node_child_process3, import_node_fs3, import_node_module, import_node_path3, import_node_os2, import_meta2;
1719
+ var init_pty_backend = __esm({
1720
+ "src/bridge/pty-backend.ts"() {
1721
+ "use strict";
1722
+ import_node_child_process3 = require("child_process");
1723
+ import_node_fs3 = require("fs");
1724
+ import_node_module = require("module");
1725
+ import_node_path3 = __toESM(require("path"), 1);
1726
+ import_node_os2 = require("os");
1727
+ init_bash_executor();
1728
+ import_meta2 = {};
1729
+ }
1730
+ });
1731
+
1732
+ // src/bridge/terminal-session.ts
1733
+ function applyTerminalBackpressure(bufferedAmount, flow) {
1734
+ if (bufferedAmount > TERMINAL_OUTPUT_HIGH_WATER) flow.pauseOutput();
1735
+ else if (bufferedAmount <= TERMINAL_OUTPUT_HIGH_WATER / 2) flow.resumeOutput();
1736
+ }
1737
+ function clampDimension(value, fallback) {
1738
+ const parsed = typeof value === "number" ? value : Number(value);
1739
+ if (!Number.isFinite(parsed)) return fallback;
1740
+ return Math.max(2, Math.min(500, Math.floor(parsed)));
1741
+ }
1742
+ var import_node_crypto, OUTPUT_CHUNK, MAX_INPUT, DEFAULT_COALESCE_MS, TERMINAL_OUTPUT_HIGH_WATER, TerminalSessionManager;
1743
+ var init_terminal_session = __esm({
1744
+ "src/bridge/terminal-session.ts"() {
1745
+ "use strict";
1746
+ import_node_crypto = require("crypto");
1747
+ init_workspace_manager();
1748
+ init_pty_backend();
1749
+ init_protocol();
1750
+ OUTPUT_CHUNK = 32 * 1024;
1751
+ MAX_INPUT = 64 * 1024;
1752
+ DEFAULT_COALESCE_MS = 16;
1753
+ TERMINAL_OUTPUT_HIGH_WATER = 1024 * 1024;
1754
+ TerminalSessionManager = class {
1755
+ session = null;
1756
+ buffer = "";
1757
+ timer = null;
1758
+ send;
1759
+ spawnPty;
1760
+ coalesceMs;
1761
+ constructor(options) {
1762
+ this.send = options.send;
1763
+ this.spawnPty = options.spawnPty ?? ((opts) => spawnHostPty(opts));
1764
+ this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
1765
+ }
1766
+ open(params = {}) {
1767
+ const cwd = resolveWorkspaceDir(params);
1768
+ const cols = clampDimension(params.cols, 80);
1769
+ const rows = clampDimension(params.rows, 24);
1770
+ this.closeActive();
1771
+ const id = (0, import_node_crypto.randomUUID)();
1772
+ const pty = this.spawnPty({ cwd, cols, rows });
1773
+ this.session = { id, pty, cwd };
1774
+ pty.onData((data) => this.pushOutput(id, data));
1775
+ pty.onExit((info) => {
1776
+ if (this.session?.id !== id) return;
1777
+ this.flush(id);
1778
+ this.session = null;
1779
+ this.send({
1780
+ jsonrpc: "2.0",
1781
+ method: TERMINAL_EVENTS.EXIT,
1782
+ params: {
1783
+ sessionId: id,
1784
+ exitCode: info.exitCode,
1785
+ signal: info.signal
1786
+ }
1787
+ });
1788
+ });
1789
+ return { sessionId: id, cwd };
1790
+ }
1791
+ input(params) {
1792
+ const session = this.requireSession(params.sessionId);
1793
+ if (typeof params.data !== "string") {
1794
+ throw new BashInvalidParamsError("data \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1795
+ }
1796
+ if (params.data.length > MAX_INPUT) {
1797
+ throw new BashInvalidParamsError("\u5355\u6B21\u8F93\u5165\u8FC7\u957F");
1798
+ }
1799
+ session.pty.write(params.data);
1800
+ return { ok: true };
1801
+ }
1802
+ resize(params) {
1803
+ const session = this.requireSession(params.sessionId);
1804
+ session.pty.resize(clampDimension(params.cols, 80), clampDimension(params.rows, 24));
1805
+ return { ok: true };
1806
+ }
1807
+ close(params = {}) {
1808
+ if (!this.session || params.sessionId && params.sessionId !== this.session.id) {
1809
+ return { ok: true };
1810
+ }
1811
+ this.closeActive();
1812
+ return { ok: true };
1813
+ }
1814
+ disposeAll() {
1815
+ this.closeActive();
1816
+ }
1817
+ pauseOutput() {
1818
+ try {
1819
+ this.session?.pty.pause();
1820
+ } catch {
1821
+ }
1822
+ }
1823
+ resumeOutput() {
1824
+ try {
1825
+ this.session?.pty.resume();
1826
+ } catch {
1827
+ }
1828
+ }
1829
+ requireSession(sessionId) {
1830
+ if (!this.session || !sessionId || sessionId !== this.session.id) {
1831
+ throw new Error("\u7EC8\u7AEF\u4F1A\u8BDD\u4E0D\u5B58\u5728");
1832
+ }
1833
+ return this.session;
1834
+ }
1835
+ closeActive() {
1836
+ const current = this.session;
1837
+ this.session = null;
1838
+ this.buffer = "";
1839
+ if (this.timer) {
1840
+ clearTimeout(this.timer);
1841
+ this.timer = null;
1842
+ }
1843
+ current?.pty.kill();
1844
+ }
1845
+ pushOutput(sessionId, chunk) {
1846
+ if (this.session?.id !== sessionId || !chunk) return;
1847
+ this.buffer += chunk;
1848
+ this.flushFullChunks(sessionId);
1849
+ if (!this.buffer) return;
1850
+ if (this.coalesceMs <= 0) {
1851
+ this.flush(sessionId);
1852
+ return;
1853
+ }
1854
+ if (!this.timer) {
1855
+ this.timer = setTimeout(() => this.flush(sessionId), this.coalesceMs);
1856
+ }
1857
+ }
1858
+ flushFullChunks(sessionId) {
1859
+ while (this.buffer.length >= OUTPUT_CHUNK) {
1860
+ const data = this.buffer.slice(0, OUTPUT_CHUNK);
1861
+ this.buffer = this.buffer.slice(OUTPUT_CHUNK);
1862
+ this.emitOutput(sessionId, data);
1863
+ }
1864
+ }
1865
+ flush(sessionId) {
1866
+ if (this.timer) {
1867
+ clearTimeout(this.timer);
1868
+ this.timer = null;
1869
+ }
1870
+ if (!this.buffer || this.session?.id !== sessionId) {
1871
+ this.buffer = "";
1872
+ return;
1873
+ }
1874
+ const data = this.buffer;
1875
+ this.buffer = "";
1876
+ this.emitOutput(sessionId, data);
1877
+ }
1878
+ emitOutput(sessionId, data) {
1879
+ this.send({
1880
+ jsonrpc: "2.0",
1881
+ method: TERMINAL_EVENTS.OUTPUT,
1882
+ params: { sessionId, data }
1883
+ });
1884
+ }
1885
+ };
1886
+ }
1887
+ });
1888
+
1310
1889
  // src/bridge/bridge-client.ts
1311
1890
  var bridge_client_exports = {};
1312
1891
  __export(bridge_client_exports, {
@@ -1319,14 +1898,18 @@ __export(bridge_client_exports, {
1319
1898
  resolveOrCreateAuthToken: () => resolveOrCreateAuthToken,
1320
1899
  verifyHmacConstantTime: () => verifyHmacConstantTime
1321
1900
  });
1901
+ function isJsonRpcId(id) {
1902
+ if (id === null || typeof id === "string") return true;
1903
+ return typeof id === "number" && Number.isFinite(id);
1904
+ }
1322
1905
  function resolveOrCreateAuthToken(explicitToken) {
1323
1906
  if (explicitToken && explicitToken.trim()) {
1324
1907
  const token = explicitToken.trim();
1325
1908
  try {
1326
- if (!import_node_fs3.default.existsSync(TOKEN_FILE_DIR)) {
1327
- import_node_fs3.default.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1909
+ if (!import_node_fs4.default.existsSync(TOKEN_FILE_DIR)) {
1910
+ import_node_fs4.default.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1328
1911
  }
1329
- import_node_fs3.default.writeFileSync(TOKEN_FILE_PATH, token, { mode: 384, encoding: "utf8" });
1912
+ import_node_fs4.default.writeFileSync(TOKEN_FILE_PATH, token, { mode: 384, encoding: "utf8" });
1330
1913
  } catch {
1331
1914
  }
1332
1915
  return token;
@@ -1335,18 +1918,18 @@ function resolveOrCreateAuthToken(explicitToken) {
1335
1918
  return process.env.ROBOT_WXT_BRIDGE_TOKEN.trim();
1336
1919
  }
1337
1920
  try {
1338
- if (import_node_fs3.default.existsSync(TOKEN_FILE_PATH)) {
1339
- const existing = import_node_fs3.default.readFileSync(TOKEN_FILE_PATH, "utf8").trim();
1921
+ if (import_node_fs4.default.existsSync(TOKEN_FILE_PATH)) {
1922
+ const existing = import_node_fs4.default.readFileSync(TOKEN_FILE_PATH, "utf8").trim();
1340
1923
  if (existing) return existing;
1341
1924
  }
1342
1925
  } catch {
1343
1926
  }
1344
- const newToken = (0, import_node_crypto.randomBytes)(32).toString("hex");
1927
+ const newToken = (0, import_node_crypto2.randomBytes)(32).toString("hex");
1345
1928
  try {
1346
- if (!import_node_fs3.default.existsSync(TOKEN_FILE_DIR)) {
1347
- import_node_fs3.default.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1929
+ if (!import_node_fs4.default.existsSync(TOKEN_FILE_DIR)) {
1930
+ import_node_fs4.default.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1348
1931
  }
1349
- import_node_fs3.default.writeFileSync(TOKEN_FILE_PATH, newToken, { mode: 384, encoding: "utf8" });
1932
+ import_node_fs4.default.writeFileSync(TOKEN_FILE_PATH, newToken, { mode: 384, encoding: "utf8" });
1350
1933
  } catch {
1351
1934
  }
1352
1935
  return newToken;
@@ -1355,10 +1938,10 @@ function parseMajorVersion(v) {
1355
1938
  return parseInt(v.split(".")[0] ?? "0", 10);
1356
1939
  }
1357
1940
  function deriveSharedSecret(tokenOrExtensionId, hmacSalt) {
1358
- return (0, import_node_crypto.createHash)("sha256").update(`${tokenOrExtensionId}:${hmacSalt}`).digest();
1941
+ return (0, import_node_crypto2.createHash)("sha256").update(`${tokenOrExtensionId}:${hmacSalt}`).digest();
1359
1942
  }
1360
1943
  function computeHmac(secret, challenge) {
1361
- return (0, import_node_crypto.createHmac)("sha256", secret).update(challenge).digest("hex");
1944
+ return (0, import_node_crypto2.createHmac)("sha256", secret).update(challenge).digest("hex");
1362
1945
  }
1363
1946
  function verifyHmacConstantTime(actualHex, expectedHex) {
1364
1947
  if (!actualHex || typeof actualHex !== "string") return false;
@@ -1366,7 +1949,7 @@ function verifyHmacConstantTime(actualHex, expectedHex) {
1366
1949
  const actualBuf = Buffer.from(actualHex, "hex");
1367
1950
  const expectedBuf = Buffer.from(expectedHex, "hex");
1368
1951
  if (actualBuf.length !== expectedBuf.length) return false;
1369
- return (0, import_node_crypto.timingSafeEqual)(actualBuf, expectedBuf);
1952
+ return (0, import_node_crypto2.timingSafeEqual)(actualBuf, expectedBuf);
1370
1953
  } catch {
1371
1954
  return false;
1372
1955
  }
@@ -1377,25 +1960,26 @@ function getKillPortHint(port) {
1377
1960
  }
1378
1961
  return `lsof -ti:${port} | xargs kill -9`;
1379
1962
  }
1380
- var import_ws, import_node_events, import_node_crypto, import_node_fs3, import_node_path3, import_node_os2, WS_PORT, REQUEST_TIMEOUT_MS, HANDSHAKE_TIMEOUT_MS, WS_OPEN, TOKEN_FILE_DIR, TOKEN_FILE_PATH, ExtensionBridgeClient;
1963
+ var import_ws, import_node_events, import_node_crypto2, import_node_fs4, import_node_path4, import_node_os3, WS_PORT, REQUEST_TIMEOUT_MS, HANDSHAKE_TIMEOUT_MS, WS_OPEN, TOKEN_FILE_DIR, TOKEN_FILE_PATH, ExtensionBridgeClient;
1381
1964
  var init_bridge_client = __esm({
1382
1965
  "src/bridge/bridge-client.ts"() {
1383
1966
  "use strict";
1384
1967
  import_ws = require("ws");
1385
1968
  import_node_events = require("events");
1386
- import_node_crypto = require("crypto");
1387
- import_node_fs3 = __toESM(require("fs"), 1);
1388
- import_node_path3 = __toESM(require("path"), 1);
1389
- import_node_os2 = __toESM(require("os"), 1);
1969
+ import_node_crypto2 = require("crypto");
1970
+ import_node_fs4 = __toESM(require("fs"), 1);
1971
+ import_node_path4 = __toESM(require("path"), 1);
1972
+ import_node_os3 = __toESM(require("os"), 1);
1390
1973
  init_protocol();
1391
1974
  init_bash_executor();
1392
1975
  init_directory_picker();
1976
+ init_terminal_session();
1393
1977
  WS_PORT = DEFAULT_WS_PORT;
1394
1978
  REQUEST_TIMEOUT_MS = 3e4;
1395
1979
  HANDSHAKE_TIMEOUT_MS = 5e3;
1396
1980
  WS_OPEN = 1;
1397
- TOKEN_FILE_DIR = import_node_path3.default.join(import_node_os2.default.homedir(), ".robot-wxt");
1398
- TOKEN_FILE_PATH = import_node_path3.default.join(TOKEN_FILE_DIR, "bridge-token");
1981
+ TOKEN_FILE_DIR = import_node_path4.default.join(import_node_os3.default.homedir(), ".robot-wxt");
1982
+ TOKEN_FILE_PATH = import_node_path4.default.join(TOKEN_FILE_DIR, "bridge-token");
1399
1983
  ExtensionBridgeClient = class _ExtensionBridgeClient extends import_node_events.EventEmitter {
1400
1984
  constructor(port = WS_PORT, authToken) {
1401
1985
  super();
@@ -1411,6 +1995,7 @@ var init_bridge_client = __esm({
1411
1995
  pending = /* @__PURE__ */ new Map();
1412
1996
  socketAuthStates = /* @__PURE__ */ new Map();
1413
1997
  destroyed = false;
1998
+ terminalSessions = null;
1414
1999
  idCounter = 0;
1415
2000
  isClientMode = false;
1416
2001
  explicitAuthToken;
@@ -1665,8 +2250,8 @@ var init_bridge_client = __esm({
1665
2250
  this.emit("error", err);
1666
2251
  });
1667
2252
  this.wss.on("connection", (socket) => {
1668
- const challenge = (0, import_node_crypto.randomUUID)();
1669
- const hmacSalt = (0, import_node_crypto.randomBytes)(16).toString("hex");
2253
+ const challenge = (0, import_node_crypto2.randomUUID)();
2254
+ const hmacSalt = (0, import_node_crypto2.randomBytes)(16).toString("hex");
1670
2255
  const authState = {
1671
2256
  authenticated: false,
1672
2257
  challenge,
@@ -1707,6 +2292,7 @@ var init_bridge_client = __esm({
1707
2292
  }
1708
2293
  if (socket === this.extensionSocket) {
1709
2294
  this.extensionSocket = null;
2295
+ this.terminalSessions?.disposeAll();
1710
2296
  this.emit("disconnected");
1711
2297
  for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1712
2298
  if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
@@ -1753,6 +2339,16 @@ var init_bridge_client = __esm({
1753
2339
  return;
1754
2340
  }
1755
2341
  if (msg.method && msg.id) {
2342
+ if (typeof msg.method === "string" && isTerminalMethod(msg.method) && socket !== this.extensionSocket) {
2343
+ this.sendJsonRpcResponse(socket, {
2344
+ id: msg.id,
2345
+ error: {
2346
+ code: BRIDGE_ERROR_CODE.METHOD_NOT_FOUND,
2347
+ message: "\u7EC8\u7AEF\u4F1A\u8BDD\u4EC5\u5141\u8BB8\u5DF2\u8BA4\u8BC1\u7684\u6269\u5C55\u8FDE\u63A5"
2348
+ }
2349
+ });
2350
+ return;
2351
+ }
1756
2352
  if (socket === this.extensionSocket) {
1757
2353
  await this.handleExtensionSystemRpc(socket, msg);
1758
2354
  return;
@@ -1825,6 +2421,7 @@ var init_bridge_client = __esm({
1825
2421
  currentAuth.clientType = msg.clientType;
1826
2422
  if (msg.clientType === "extension") {
1827
2423
  if (this.extensionSocket && this.extensionSocket !== socket) {
2424
+ this.terminalSessions?.disposeAll();
1828
2425
  try {
1829
2426
  this.extensionSocket.close(WS_CLOSE_CODE.REPLACED, "Replaced by new connection");
1830
2427
  } catch {
@@ -1916,9 +2513,6 @@ var init_bridge_client = __esm({
1916
2513
  systemRpcHandlers = Object.assign(
1917
2514
  /* @__PURE__ */ Object.create(null),
1918
2515
  {
1919
- [BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
1920
- return executeBash(params || {});
1921
- },
1922
2516
  [BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
1923
2517
  const selectedPath = await pickDirectory({
1924
2518
  title: params?.title || void 0,
@@ -1929,11 +2523,61 @@ var init_bridge_client = __esm({
1929
2523
  selectedPath,
1930
2524
  canceled: selectedPath === null
1931
2525
  };
1932
- }
2526
+ },
2527
+ [BRIDGE_METHODS.TERMINAL_OPEN]: async (params) => this.getTerminalSessions().open(params || {}),
2528
+ [BRIDGE_METHODS.TERMINAL_INPUT]: async (params) => this.getTerminalSessions().input(params || {}),
2529
+ [BRIDGE_METHODS.TERMINAL_RESIZE]: async (params) => this.getTerminalSessions().resize(params || {}),
2530
+ [BRIDGE_METHODS.TERMINAL_CLOSE]: async (params) => this.getTerminalSessions().close(params || {})
1933
2531
  }
1934
2532
  );
2533
+ sendJsonRpcNotification(socket, method, params) {
2534
+ if (socket.readyState !== WS_OPEN) return;
2535
+ let payload;
2536
+ try {
2537
+ payload = JSON.stringify({ jsonrpc: "2.0", method, params });
2538
+ } catch {
2539
+ return;
2540
+ }
2541
+ try {
2542
+ socket.send(payload);
2543
+ } catch {
2544
+ }
2545
+ }
2546
+ async executeHostBash(socket, id, params) {
2547
+ try {
2548
+ const requestParams = params && typeof params === "object" ? params : {};
2549
+ const result = await executeBash({
2550
+ ...requestParams,
2551
+ onOutput: (chunk) => {
2552
+ if (!chunk.text) return;
2553
+ this.sendJsonRpcNotification(socket, BASH_EVENTS.OUTPUT, {
2554
+ id,
2555
+ stream: chunk.stream,
2556
+ text: chunk.text
2557
+ });
2558
+ }
2559
+ });
2560
+ this.sendJsonRpcResponse(socket, { id, result });
2561
+ } catch (err) {
2562
+ this.sendJsonRpcResponse(socket, { id, error: this.formatSystemRpcError(err) });
2563
+ }
2564
+ }
1935
2565
  async handleExtensionSystemRpc(socket, msg) {
1936
2566
  const { id, method, params } = msg;
2567
+ if (!isJsonRpcId(id)) {
2568
+ this.sendJsonRpcResponse(socket, {
2569
+ id: null,
2570
+ error: {
2571
+ code: BRIDGE_ERROR_CODE.INVALID_PARAMS,
2572
+ message: "\u65E0\u6548\u7684\u8BF7\u6C42 id"
2573
+ }
2574
+ });
2575
+ return;
2576
+ }
2577
+ if (method === BRIDGE_METHODS.SYSTEM_BASH) {
2578
+ await this.executeHostBash(socket, id, params);
2579
+ return;
2580
+ }
1937
2581
  const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
1938
2582
  if (!handler || typeof handler !== "function") {
1939
2583
  this.sendJsonRpcResponse(socket, {
@@ -2029,9 +2673,37 @@ var init_bridge_client = __esm({
2029
2673
  }
2030
2674
  });
2031
2675
  }
2676
+ getTerminalSessions() {
2677
+ if (!this.terminalSessions) {
2678
+ this.terminalSessions = new TerminalSessionManager({
2679
+ send: (frame) => {
2680
+ const socket = this.extensionSocket;
2681
+ if (!socket || socket.readyState !== WS_OPEN) return;
2682
+ const sessions = this.terminalSessions;
2683
+ if (sessions) applyTerminalBackpressure(socket.bufferedAmount ?? 0, sessions);
2684
+ let payload;
2685
+ try {
2686
+ payload = JSON.stringify(frame);
2687
+ } catch {
2688
+ return;
2689
+ }
2690
+ try {
2691
+ socket.send(payload, () => {
2692
+ if (this.terminalSessions && socket.readyState === WS_OPEN) {
2693
+ applyTerminalBackpressure(socket.bufferedAmount ?? 0, this.terminalSessions);
2694
+ }
2695
+ });
2696
+ } catch {
2697
+ }
2698
+ }
2699
+ });
2700
+ }
2701
+ return this.terminalSessions;
2702
+ }
2032
2703
  /** 销毁服务,断开所有连接 */
2033
2704
  destroy() {
2034
2705
  this.destroyed = true;
2706
+ this.terminalSessions?.disposeAll();
2035
2707
  this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
2036
2708
  this.forwardedRequests.clear();
2037
2709
  if (this.clientSocket) {
@@ -2176,16 +2848,16 @@ __export(daemon_exports, {
2176
2848
  isDaemonProcessAlive: () => isDaemonProcessAlive,
2177
2849
  isProcessAlive: () => isProcessAlive,
2178
2850
  killProcessOnPort: () => killProcessOnPort,
2179
- killProcessTree: () => killProcessTree2,
2851
+ killProcessTree: () => killProcessTree,
2180
2852
  probeServerReady: () => probeServerReady,
2181
2853
  runDaemonProcess: () => runDaemonProcess,
2182
2854
  stopBridgeDaemon: () => stopBridgeDaemon
2183
2855
  });
2184
2856
  function getDaemonPidFile(port = WS_PORT2) {
2185
2857
  if (port === WS_PORT2) {
2186
- return import_node_path4.default.join(DAEMON_DIR, "daemon.pid");
2858
+ return import_node_path5.default.join(DAEMON_DIR, "daemon.pid");
2187
2859
  }
2188
- return import_node_path4.default.join(DAEMON_DIR, `daemon-${port}.pid`);
2860
+ return import_node_path5.default.join(DAEMON_DIR, `daemon-${port}.pid`);
2189
2861
  }
2190
2862
  function isProcessAlive(pid) {
2191
2863
  if (!pid || pid <= 0) return false;
@@ -2229,7 +2901,7 @@ async function findPidByPort(port, excludePid) {
2229
2901
  return null;
2230
2902
  }
2231
2903
  }
2232
- async function killProcessTree2(pid) {
2904
+ async function killProcessTree(pid) {
2233
2905
  if (!pid || pid <= 0 || pid === process.pid) return false;
2234
2906
  if (process.platform === "win32") {
2235
2907
  try {
@@ -2267,7 +2939,7 @@ async function killProcessTree2(pid) {
2267
2939
  async function killProcessOnPort(port) {
2268
2940
  const pid = await findPidByPort(port, process.pid);
2269
2941
  if (pid) {
2270
- await killProcessTree2(pid);
2942
+ await killProcessTree(pid);
2271
2943
  for (let i = 0; i < 20; i++) {
2272
2944
  await new Promise((r) => setTimeout(r, 100));
2273
2945
  const remainingPid = await findPidByPort(port, process.pid);
@@ -2281,9 +2953,9 @@ async function probeServerReady(port = WS_PORT2, token, timeoutMs = 1e3) {
2281
2953
  }
2282
2954
  function isDaemonProcessAlive(port = WS_PORT2) {
2283
2955
  const pidFile = getDaemonPidFile(port);
2284
- if (!import_node_fs4.default.existsSync(pidFile)) return false;
2956
+ if (!import_node_fs5.default.existsSync(pidFile)) return false;
2285
2957
  try {
2286
- const pid = parseInt(import_node_fs4.default.readFileSync(pidFile, "utf8").trim(), 10);
2958
+ const pid = parseInt(import_node_fs5.default.readFileSync(pidFile, "utf8").trim(), 10);
2287
2959
  return Boolean(pid && isProcessAlive(pid));
2288
2960
  } catch {
2289
2961
  return false;
@@ -2331,19 +3003,19 @@ async function ensureBridgeDaemon(options = {}) {
2331
3003
  throw new Error(`\u542F\u52A8\u540E\u53F0 WebSocket \u6865\u63A5\u670D\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7AEF\u53E3 ${port} \u662F\u5426\u53EF\u7528\u3002`);
2332
3004
  }
2333
3005
  function resolveCliEntry() {
2334
- const currentDir = import_node_path4.default.dirname((0, import_node_url.fileURLToPath)(import_meta2.url));
2335
- const distCandidate = import_node_path4.default.join(currentDir, "bin.js");
2336
- if (import_node_fs4.default.existsSync(distCandidate)) return distCandidate;
2337
- const projectDistCandidate = import_node_path4.default.resolve(currentDir, "../dist/bin.js");
2338
- if (import_node_fs4.default.existsSync(projectDistCandidate)) return projectDistCandidate;
3006
+ const currentDir = import_node_path5.default.dirname((0, import_node_url.fileURLToPath)(import_meta3.url));
3007
+ const distCandidate = import_node_path5.default.join(currentDir, "bin.js");
3008
+ if (import_node_fs5.default.existsSync(distCandidate)) return distCandidate;
3009
+ const projectDistCandidate = import_node_path5.default.resolve(currentDir, "../dist/bin.js");
3010
+ if (import_node_fs5.default.existsSync(projectDistCandidate)) return projectDistCandidate;
2339
3011
  return distCandidate;
2340
3012
  }
2341
3013
  function startDaemonProcessBackground(token, port) {
2342
3014
  const cliEntry = resolveCliEntry();
2343
- if (!import_node_fs4.default.existsSync(DAEMON_DIR)) {
2344
- import_node_fs4.default.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
3015
+ if (!import_node_fs5.default.existsSync(DAEMON_DIR)) {
3016
+ import_node_fs5.default.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
2345
3017
  }
2346
- const child = (0, import_node_child_process3.spawn)(process.execPath, [cliEntry, "--mode", "wxt", "daemon", "--port", String(port)], {
3018
+ const child = (0, import_node_child_process4.spawn)(process.execPath, [cliEntry, "--mode", "wxt", "daemon", "--port", String(port)], {
2347
3019
  detached: true,
2348
3020
  stdio: "ignore",
2349
3021
  windowsHide: true,
@@ -2376,20 +3048,20 @@ async function runDaemonProcess(args) {
2376
3048
  const pidFile = getDaemonPidFile(port);
2377
3049
  client.on("listening", () => {
2378
3050
  try {
2379
- if (!import_node_fs4.default.existsSync(DAEMON_DIR)) {
2380
- import_node_fs4.default.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
3051
+ if (!import_node_fs5.default.existsSync(DAEMON_DIR)) {
3052
+ import_node_fs5.default.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
2381
3053
  }
2382
- import_node_fs4.default.writeFileSync(pidFile, String(process.pid), { mode: 384, encoding: "utf8" });
3054
+ import_node_fs5.default.writeFileSync(pidFile, String(process.pid), { mode: 384, encoding: "utf8" });
2383
3055
  } catch {
2384
3056
  }
2385
3057
  });
2386
3058
  client.connect();
2387
3059
  const shutdown = () => {
2388
3060
  try {
2389
- if (import_node_fs4.default.existsSync(pidFile)) {
2390
- const recordedPid = import_node_fs4.default.readFileSync(pidFile, "utf8").trim();
3061
+ if (import_node_fs5.default.existsSync(pidFile)) {
3062
+ const recordedPid = import_node_fs5.default.readFileSync(pidFile, "utf8").trim();
2391
3063
  if (recordedPid === String(process.pid)) {
2392
- import_node_fs4.default.unlinkSync(pidFile);
3064
+ import_node_fs5.default.unlinkSync(pidFile);
2393
3065
  }
2394
3066
  }
2395
3067
  } catch {
@@ -2427,9 +3099,9 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2427
3099
  let killedAny = false;
2428
3100
  const pidFile = getDaemonPidFile(port);
2429
3101
  let daemonPid = null;
2430
- if (import_node_fs4.default.existsSync(pidFile)) {
3102
+ if (import_node_fs5.default.existsSync(pidFile)) {
2431
3103
  try {
2432
- const pidStr = import_node_fs4.default.readFileSync(pidFile, "utf8").trim();
3104
+ const pidStr = import_node_fs5.default.readFileSync(pidFile, "utf8").trim();
2433
3105
  const p = parseInt(pidStr, 10);
2434
3106
  if (p && isProcessAlive(p)) {
2435
3107
  daemonPid = p;
@@ -2449,22 +3121,22 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2449
3121
  if (portPid && portPid !== daemonPid) {
2450
3122
  return false;
2451
3123
  }
2452
- const killed = await killProcessTree2(daemonPid);
3124
+ const killed = await killProcessTree(daemonPid);
2453
3125
  if (killed) {
2454
3126
  killedAny = true;
2455
3127
  try {
2456
- if (import_node_fs4.default.existsSync(pidFile)) import_node_fs4.default.unlinkSync(pidFile);
3128
+ if (import_node_fs5.default.existsSync(pidFile)) import_node_fs5.default.unlinkSync(pidFile);
2457
3129
  } catch {
2458
3130
  }
2459
3131
  }
2460
3132
  } else {
2461
3133
  if (daemonPid) {
2462
3134
  try {
2463
- const killed = await killProcessTree2(daemonPid);
3135
+ const killed = await killProcessTree(daemonPid);
2464
3136
  if (killed) {
2465
3137
  killedAny = true;
2466
3138
  try {
2467
- if (import_node_fs4.default.existsSync(pidFile)) import_node_fs4.default.unlinkSync(pidFile);
3139
+ if (import_node_fs5.default.existsSync(pidFile)) import_node_fs5.default.unlinkSync(pidFile);
2468
3140
  } catch {
2469
3141
  }
2470
3142
  }
@@ -2493,21 +3165,21 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2493
3165
  }
2494
3166
  return killedAny;
2495
3167
  }
2496
- var import_node_child_process3, import_node_util, import_node_fs4, import_node_path4, import_node_os3, import_node_url, import_meta2, execAsync, WS_PORT2, DAEMON_DIR, PID_FILE;
3168
+ var import_node_child_process4, import_node_util, import_node_fs5, import_node_path5, import_node_os4, import_node_url, import_meta3, execAsync, WS_PORT2, DAEMON_DIR, PID_FILE;
2497
3169
  var init_daemon = __esm({
2498
3170
  "src/bridge/daemon.ts"() {
2499
3171
  "use strict";
2500
- import_node_child_process3 = require("child_process");
3172
+ import_node_child_process4 = require("child_process");
2501
3173
  import_node_util = require("util");
2502
- import_node_fs4 = __toESM(require("fs"), 1);
2503
- import_node_path4 = __toESM(require("path"), 1);
2504
- import_node_os3 = __toESM(require("os"), 1);
3174
+ import_node_fs5 = __toESM(require("fs"), 1);
3175
+ import_node_path5 = __toESM(require("path"), 1);
3176
+ import_node_os4 = __toESM(require("os"), 1);
2505
3177
  import_node_url = require("url");
2506
3178
  init_bridge_client();
2507
- import_meta2 = {};
2508
- execAsync = (0, import_node_util.promisify)(import_node_child_process3.exec);
3179
+ import_meta3 = {};
3180
+ execAsync = (0, import_node_util.promisify)(import_node_child_process4.exec);
2509
3181
  WS_PORT2 = 18999;
2510
- DAEMON_DIR = import_node_path4.default.join(import_node_os3.default.homedir(), ".robot-wxt");
3182
+ DAEMON_DIR = import_node_path5.default.join(import_node_os4.default.homedir(), ".robot-wxt");
2511
3183
  PID_FILE = getDaemonPidFile(WS_PORT2);
2512
3184
  }
2513
3185
  });