@opentiny/webmcp-cli 0.1.0-alpha.1 → 0.1.0-beta.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-alpha.1",
551
+ version: "0.1.0-beta.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: {
@@ -579,7 +587,7 @@ var init_package = __esm({
579
587
  license: "MIT",
580
588
  author: "OpenTiny Team",
581
589
  engines: {
582
- node: ">=16"
590
+ node: ">=18.19.0"
583
591
  },
584
592
  publishConfig: {
585
593
  access: "public"
@@ -591,6 +599,7 @@ var init_package = __esm({
591
599
  "@page-agent/page-controller": "catalog:",
592
600
  clipboardy: "^5.3.1",
593
601
  commander: "^11.1.0",
602
+ execa: "^9.6.1",
594
603
  json5: "^2.2.3",
595
604
  marked: "^15.0.7",
596
605
  picocolors: "^1.0.0",
@@ -819,12 +828,15 @@ var init_cdp_adapter = __esm({
819
828
  });
820
829
 
821
830
  // src/bridge/protocol.ts
822
- var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS;
831
+ function isTerminalMethod(method) {
832
+ return TERMINAL_METHOD_SET.has(method);
833
+ }
834
+ var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS, TERMINAL_EVENTS, BASH_EVENTS, TERMINAL_METHOD_SET;
823
835
  var init_protocol = __esm({
824
836
  "src/bridge/protocol.ts"() {
825
837
  "use strict";
826
838
  DEFAULT_WS_PORT = 18999;
827
- PROTOCOL_VERSION = "1.1.0";
839
+ PROTOCOL_VERSION = "1.2.0";
828
840
  WS_CLOSE_CODE = {
829
841
  /** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
830
842
  REPLACED: 4e3,
@@ -858,8 +870,25 @@ var init_protocol = __esm({
858
870
  SKILLS_GET: "skills/get",
859
871
  SKILLS_READ_RESOURCE: "skills/read_resource",
860
872
  SYSTEM_BASH: "system/bash",
861
- SYSTEM_PICK_DIRECTORY: "system/pick_directory"
873
+ SYSTEM_PICK_DIRECTORY: "system/pick_directory",
874
+ TERMINAL_OPEN: "terminal/open",
875
+ TERMINAL_INPUT: "terminal/input",
876
+ TERMINAL_RESIZE: "terminal/resize",
877
+ TERMINAL_CLOSE: "terminal/close"
862
878
  };
879
+ TERMINAL_EVENTS = {
880
+ OUTPUT: "notification/terminal_output",
881
+ EXIT: "notification/terminal_exit"
882
+ };
883
+ BASH_EVENTS = {
884
+ OUTPUT: "notification/bash_output"
885
+ };
886
+ TERMINAL_METHOD_SET = /* @__PURE__ */ new Set([
887
+ BRIDGE_METHODS.TERMINAL_OPEN,
888
+ BRIDGE_METHODS.TERMINAL_INPUT,
889
+ BRIDGE_METHODS.TERMINAL_RESIZE,
890
+ BRIDGE_METHODS.TERMINAL_CLOSE
891
+ ]);
863
892
  }
864
893
  });
865
894
 
@@ -968,25 +997,56 @@ function resolveShell(forcePlatform) {
968
997
  if (!forcePlatform) cachedShell = fallback;
969
998
  return fallback;
970
999
  }
971
- function killProcessTree(child) {
972
- if (process.platform === "win32" && child.pid) {
973
- try {
974
- (0, import_node_child_process.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
975
- windowsHide: true,
976
- stdio: "ignore"
977
- });
978
- } catch {
979
- child.kill("SIGKILL");
1000
+ function withoutCarriageReturn(buf) {
1001
+ if (!buf.includes(13)) return buf;
1002
+ const out = Buffer.allocUnsafe(buf.length);
1003
+ let size = 0;
1004
+ for (let index = 0; index < buf.length; index += 1) {
1005
+ const byte = buf[index];
1006
+ if (byte === 13) continue;
1007
+ out[size] = byte;
1008
+ size += 1;
1009
+ }
1010
+ return out.subarray(0, size);
1011
+ }
1012
+ function collectLimited(stream, maxBuffer, onText, stripCarriageReturn = false) {
1013
+ const state = { chunks: [], truncated: false };
1014
+ if (!stream) return state;
1015
+ const decoder = new import_node_string_decoder.StringDecoder("utf8");
1016
+ let bytes = 0;
1017
+ const emit = (buf) => {
1018
+ const text = decoder.write(buf);
1019
+ if (text) onText?.(text);
1020
+ };
1021
+ stream.on("data", (chunk) => {
1022
+ const raw = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1023
+ const buf = stripCarriageReturn ? withoutCarriageReturn(raw) : raw;
1024
+ if (buf.length === 0) return;
1025
+ if (bytes >= maxBuffer) {
1026
+ state.truncated = true;
1027
+ return;
980
1028
  }
981
- } else {
982
- child.kill("SIGTERM");
983
- setTimeout(() => {
984
- try {
985
- child.kill("SIGKILL");
986
- } catch {
987
- }
988
- }, 2e3);
989
- }
1029
+ const remaining = maxBuffer - bytes;
1030
+ if (buf.length <= remaining) {
1031
+ state.chunks.push(buf);
1032
+ bytes += buf.length;
1033
+ emit(buf);
1034
+ return;
1035
+ }
1036
+ const kept = buf.subarray(0, remaining);
1037
+ state.chunks.push(kept);
1038
+ bytes += remaining;
1039
+ state.truncated = true;
1040
+ emit(kept);
1041
+ });
1042
+ stream.on("end", () => {
1043
+ const rest = decoder.end();
1044
+ if (rest) onText?.(rest);
1045
+ });
1046
+ return state;
1047
+ }
1048
+ function decodeLimited(state) {
1049
+ return Buffer.concat(state.chunks).toString("utf8").trim();
990
1050
  }
991
1051
  async function executeBash(params) {
992
1052
  const trimmedCommand = params.command?.trim();
@@ -1005,83 +1065,66 @@ async function executeBash(params) {
1005
1065
  });
1006
1066
  const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
1007
1067
  const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
1008
- const startTime = Date.now();
1009
1068
  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
- });
1069
+ const useTty = params.tty === true && process.platform !== "win32";
1070
+ const subprocess = useTty ? (0, import_execa.execa)("python3", ["-u", "-c", TTY_HELPER], {
1071
+ cwd,
1072
+ env: {
1073
+ ...env,
1074
+ BASH_TTY_COMMAND: trimmedCommand,
1075
+ BASH_TTY_SHELL: shell
1076
+ },
1077
+ stdin: "ignore",
1078
+ buffer: false,
1079
+ encoding: "buffer",
1080
+ reject: false,
1081
+ timeout: normalizedTimeoutMs,
1082
+ forceKillAfterDelay: FORCE_KILL_AFTER_DELAY_MS,
1083
+ windowsHide: true,
1084
+ stripFinalNewline: false
1085
+ }) : (0, import_execa.execa)(trimmedCommand, {
1086
+ shell,
1087
+ cwd,
1088
+ env,
1089
+ stdin: "ignore",
1090
+ buffer: false,
1091
+ encoding: "buffer",
1092
+ reject: false,
1093
+ timeout: normalizedTimeoutMs,
1094
+ forceKillAfterDelay: FORCE_KILL_AFTER_DELAY_MS,
1095
+ windowsHide: true,
1096
+ stripFinalNewline: false
1076
1097
  });
1098
+ const emit = (stream) => (text) => {
1099
+ params.onOutput?.({ stream, text });
1100
+ };
1101
+ const stdoutState = collectLimited(subprocess.stdout, maxBuffer, emit("stdout"), useTty);
1102
+ const stderrState = collectLimited(subprocess.stderr, maxBuffer, emit("stderr"));
1103
+ const result = await subprocess;
1104
+ if (result.timedOut) {
1105
+ throw new BashTimeoutError(normalizedTimeoutMs);
1106
+ }
1107
+ if (result.failed && result.exitCode == null && result.signal == null && !result.isTerminated) {
1108
+ throw new Error(result.shortMessage || result.message || "\u547D\u4EE4\u542F\u52A8\u5931\u8D25");
1109
+ }
1110
+ const truncated = stdoutState.truncated || stderrState.truncated;
1111
+ return {
1112
+ stdout: decodeLimited(stdoutState),
1113
+ stderr: decodeLimited(stderrState),
1114
+ exitCode: result.exitCode ?? null,
1115
+ signal: result.signal ?? null,
1116
+ durationMs: result.durationMs ?? 0,
1117
+ ...truncated ? { truncated: true } : {}
1118
+ };
1077
1119
  }
1078
- var import_node_fs2, import_node_path2, import_node_child_process, BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell;
1120
+ var 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;
1079
1121
  var init_bash_executor = __esm({
1080
1122
  "src/bridge/bash-executor.ts"() {
1081
1123
  "use strict";
1082
1124
  import_node_fs2 = __toESM(require("fs"), 1);
1083
1125
  import_node_path2 = __toESM(require("path"), 1);
1084
- import_node_child_process = require("child_process");
1126
+ import_node_string_decoder = require("string_decoder");
1127
+ import_execa = require("execa");
1085
1128
  init_workspace_manager();
1086
1129
  BashTimeoutError = class extends Error {
1087
1130
  code = "TIMEOUT";
@@ -1096,6 +1139,46 @@ var init_bash_executor = __esm({
1096
1139
  MAX_TIMEOUT_MS = 6e5;
1097
1140
  DEFAULT_MAX_BUFFER = 512 * 1024;
1098
1141
  cachedShell = null;
1142
+ FORCE_KILL_AFTER_DELAY_MS = 2e3;
1143
+ TTY_HELPER = `
1144
+ import errno, os, sys
1145
+ cmd = os.environ.get("BASH_TTY_COMMAND") or ""
1146
+ shell = os.environ.get("BASH_TTY_SHELL") or "/bin/bash"
1147
+ if not cmd:
1148
+ sys.exit(127)
1149
+ import pty
1150
+ pid, fd = pty.fork()
1151
+ if pid == 0:
1152
+ os.environ.pop("BASH_TTY_COMMAND", None)
1153
+ os.environ.pop("BASH_TTY_SHELL", None)
1154
+ os.environ["TERM"] = "xterm-256color"
1155
+ try:
1156
+ os.execvp(shell, [shell, "-c", cmd])
1157
+ except OSError:
1158
+ os._exit(127)
1159
+ while True:
1160
+ try:
1161
+ data = os.read(fd, 65536)
1162
+ except OSError as exc:
1163
+ if exc.errno == errno.EINTR:
1164
+ continue
1165
+ break
1166
+ if not data:
1167
+ break
1168
+ try:
1169
+ os.write(1, data)
1170
+ except OSError:
1171
+ break
1172
+ try:
1173
+ _, status = os.waitpid(pid, 0)
1174
+ except OSError:
1175
+ sys.exit(1)
1176
+ if os.WIFEXITED(status):
1177
+ sys.exit(os.WEXITSTATUS(status))
1178
+ if os.WIFSIGNALED(status):
1179
+ sys.exit(128 + os.WTERMSIG(status))
1180
+ sys.exit(1)
1181
+ `;
1099
1182
  }
1100
1183
  });
1101
1184
 
@@ -1118,7 +1201,7 @@ function pickDirectoryMac(title, defaultPath) {
1118
1201
  const defaultClause = defaultPath ? ` default location POSIX file "${defaultPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : "";
1119
1202
  const appleScript = `POSIX path of (choose folder with prompt "${safeTitle}"${defaultClause})`;
1120
1203
  return new Promise((resolve3, reject) => {
1121
- const child = (0, import_node_child_process2.spawn)("osascript", ["-e", appleScript], {
1204
+ const child = (0, import_node_child_process.spawn)("osascript", ["-e", appleScript], {
1122
1205
  stdio: ["ignore", "pipe", "pipe"]
1123
1206
  });
1124
1207
  let stdout = "";
@@ -1236,7 +1319,7 @@ $result = [FolderPicker]::Pick("${safeTitle}", "${safeDefaultPath}")
1236
1319
  if ($result) { Write-Output $result }
1237
1320
  `;
1238
1321
  return new Promise((resolve3, reject) => {
1239
- const child = (0, import_node_child_process2.spawn)("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1322
+ const child = (0, import_node_child_process.spawn)("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1240
1323
  stdio: ["ignore", "pipe", "pipe"],
1241
1324
  windowsHide: true
1242
1325
  });
@@ -1267,7 +1350,7 @@ function pickDirectoryLinux(title, defaultPath) {
1267
1350
  args.push(`--filename=${defaultPath}`);
1268
1351
  }
1269
1352
  return new Promise((resolve3, reject) => {
1270
- const child = (0, import_node_child_process2.spawn)("zenity", args, {
1353
+ const child = (0, import_node_child_process.spawn)("zenity", args, {
1271
1354
  stdio: ["ignore", "pipe", "pipe"]
1272
1355
  });
1273
1356
  let stdout = "";
@@ -1299,11 +1382,349 @@ function pickDirectoryLinux(title, defaultPath) {
1299
1382
  });
1300
1383
  });
1301
1384
  }
1302
- var import_node_child_process2;
1385
+ var import_node_child_process;
1303
1386
  var init_directory_picker = __esm({
1304
1387
  "src/bridge/directory-picker.ts"() {
1388
+ "use strict";
1389
+ import_node_child_process = require("child_process");
1390
+ }
1391
+ });
1392
+
1393
+ // src/bridge/pty-backend.ts
1394
+ function spawnHostPty(options) {
1395
+ if (process.platform === "win32") {
1396
+ throw new Error("\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F\u7EC8\u7AEF");
1397
+ }
1398
+ const shell = options.shell || resolveShell();
1399
+ const child = (0, import_node_child_process2.spawn)("python3", ["-u", "-c", PTY_HELPER], {
1400
+ cwd: options.cwd,
1401
+ env: {
1402
+ ...process.env,
1403
+ TERM: "xterm-256color",
1404
+ TERMINAL_SHELL: shell,
1405
+ TERMINAL_ROWS: String(options.rows),
1406
+ TERMINAL_COLS: String(options.cols)
1407
+ },
1408
+ stdio: ["pipe", "pipe", "pipe", "pipe"]
1409
+ });
1410
+ const pending = [];
1411
+ let dataCb = null;
1412
+ let exitCb = null;
1413
+ let pendingExit = null;
1414
+ let settled = false;
1415
+ const decoder = new TextDecoder("utf-8", { fatal: false });
1416
+ let stderrText = "";
1417
+ const emitData = (data) => {
1418
+ if (!data) return;
1419
+ if (dataCb) dataCb(data);
1420
+ else pending.push(data);
1421
+ };
1422
+ const finish = (info) => {
1423
+ if (settled) return;
1424
+ settled = true;
1425
+ if (stderrText.trim()) emitData(`\r
1426
+ ${stderrText.trim()}\r
1427
+ `);
1428
+ if (exitCb) exitCb(info);
1429
+ else pendingExit = info;
1430
+ };
1431
+ child.stdout.on("data", (buf) => {
1432
+ emitData(decoder.decode(buf, { stream: true }));
1433
+ });
1434
+ child.stderr.on("data", (buf) => {
1435
+ stderrText += buf.toString("utf8");
1436
+ });
1437
+ child.on("error", (err) => {
1438
+ emitData(`\r
1439
+ [\u7EC8\u7AEF\u542F\u52A8\u5931\u8D25] ${err.message}\r
1440
+ `);
1441
+ finish({ exitCode: null, signal: null });
1442
+ });
1443
+ child.on("exit", (code, signal) => {
1444
+ const rest = decoder.decode();
1445
+ if (rest) emitData(rest);
1446
+ finish({ exitCode: code, signal: signal ?? null });
1447
+ });
1448
+ const control = child.stdio[3];
1449
+ const writeControl = (payload) => {
1450
+ if (!control || !("write" in control)) return;
1451
+ try {
1452
+ ;
1453
+ control.write(`${JSON.stringify(payload)}
1454
+ `);
1455
+ } catch {
1456
+ }
1457
+ };
1458
+ return {
1459
+ write(data) {
1460
+ try {
1461
+ child.stdin.write(data);
1462
+ } catch {
1463
+ }
1464
+ },
1465
+ resize(cols, rows) {
1466
+ writeControl({ type: "resize", cols, rows });
1467
+ },
1468
+ kill() {
1469
+ writeControl({ type: "kill" });
1470
+ try {
1471
+ child.kill("SIGTERM");
1472
+ } catch {
1473
+ }
1474
+ },
1475
+ onData(cb) {
1476
+ dataCb = cb;
1477
+ for (const chunk of pending) cb(chunk);
1478
+ pending.length = 0;
1479
+ },
1480
+ onExit(cb) {
1481
+ exitCb = cb;
1482
+ if (pendingExit) cb(pendingExit);
1483
+ }
1484
+ };
1485
+ }
1486
+ var import_node_child_process2, PTY_HELPER;
1487
+ var init_pty_backend = __esm({
1488
+ "src/bridge/pty-backend.ts"() {
1305
1489
  "use strict";
1306
1490
  import_node_child_process2 = require("child_process");
1491
+ init_bash_executor();
1492
+ PTY_HELPER = `
1493
+ import errno, fcntl, json, os, pty, select, signal, struct, sys, termios
1494
+
1495
+ shell = os.environ.get("TERMINAL_SHELL") or "/bin/bash"
1496
+ rows = int(os.environ.get("TERMINAL_ROWS") or "24")
1497
+ cols = int(os.environ.get("TERMINAL_COLS") or "80")
1498
+ control_fd = 3
1499
+
1500
+ def set_winsize(fd, next_rows, next_cols):
1501
+ winsize = struct.pack("HHHH", int(next_rows), int(next_cols), 0, 0)
1502
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
1503
+
1504
+ pid, master = pty.fork()
1505
+ if pid == 0:
1506
+ try:
1507
+ os.close(control_fd)
1508
+ except OSError:
1509
+ pass
1510
+ os.environ["TERM"] = "xterm-256color"
1511
+ try:
1512
+ os.execvp(shell, [shell, "-i"])
1513
+ except OSError:
1514
+ os._exit(127)
1515
+
1516
+ def control_loop():
1517
+ try:
1518
+ reader = os.fdopen(control_fd, "r", buffering=1, encoding="utf-8", errors="replace")
1519
+ except OSError:
1520
+ return
1521
+ for line in reader:
1522
+ text = line.strip()
1523
+ if not text:
1524
+ continue
1525
+ try:
1526
+ msg = json.loads(text)
1527
+ except json.JSONDecodeError:
1528
+ continue
1529
+ kind = msg.get("type")
1530
+ if kind == "resize":
1531
+ try:
1532
+ set_winsize(master, msg.get("rows") or rows, msg.get("cols") or cols)
1533
+ os.kill(pid, signal.SIGWINCH)
1534
+ except OSError:
1535
+ pass
1536
+ elif kind == "kill":
1537
+ try:
1538
+ os.kill(pid, signal.SIGHUP)
1539
+ except OSError:
1540
+ pass
1541
+ os._exit(0)
1542
+
1543
+ import threading
1544
+ threading.Thread(target=control_loop, daemon=True).start()
1545
+
1546
+ try:
1547
+ set_winsize(master, rows, cols)
1548
+ except OSError:
1549
+ pass
1550
+
1551
+ stdin_fd = sys.stdin.fileno()
1552
+ stdout_fd = sys.stdout.fileno()
1553
+
1554
+ while True:
1555
+ try:
1556
+ readable, _, _ = select.select([master, stdin_fd], [], [])
1557
+ except InterruptedError:
1558
+ continue
1559
+ if master in readable:
1560
+ try:
1561
+ data = os.read(master, 65536)
1562
+ except OSError as err:
1563
+ if err.errno == errno.EIO:
1564
+ break
1565
+ raise
1566
+ if not data:
1567
+ break
1568
+ os.write(stdout_fd, data)
1569
+ if stdin_fd in readable:
1570
+ try:
1571
+ data = os.read(stdin_fd, 65536)
1572
+ except OSError:
1573
+ break
1574
+ if not data:
1575
+ break
1576
+ try:
1577
+ os.write(master, data)
1578
+ except OSError:
1579
+ break
1580
+
1581
+ try:
1582
+ _, status = os.waitpid(pid, 0)
1583
+ except ChildProcessError:
1584
+ status = 0
1585
+ code = os.waitstatus_to_exitcode(status)
1586
+ os._exit(0 if code < 0 else code)
1587
+ `;
1588
+ }
1589
+ });
1590
+
1591
+ // src/bridge/terminal-session.ts
1592
+ function clampDimension(value, fallback) {
1593
+ const parsed = typeof value === "number" ? value : Number(value);
1594
+ if (!Number.isFinite(parsed)) return fallback;
1595
+ return Math.max(2, Math.min(500, Math.floor(parsed)));
1596
+ }
1597
+ var import_node_crypto, OUTPUT_CHUNK, MAX_INPUT, DEFAULT_COALESCE_MS, TerminalSessionManager;
1598
+ var init_terminal_session = __esm({
1599
+ "src/bridge/terminal-session.ts"() {
1600
+ "use strict";
1601
+ import_node_crypto = require("crypto");
1602
+ init_workspace_manager();
1603
+ init_pty_backend();
1604
+ init_protocol();
1605
+ OUTPUT_CHUNK = 32 * 1024;
1606
+ MAX_INPUT = 64 * 1024;
1607
+ DEFAULT_COALESCE_MS = 16;
1608
+ TerminalSessionManager = class {
1609
+ session = null;
1610
+ buffer = "";
1611
+ timer = null;
1612
+ send;
1613
+ spawnPty;
1614
+ coalesceMs;
1615
+ constructor(options) {
1616
+ this.send = options.send;
1617
+ this.spawnPty = options.spawnPty ?? ((opts) => spawnHostPty(opts));
1618
+ this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
1619
+ }
1620
+ open(params = {}) {
1621
+ const cwd = resolveWorkspaceDir(params);
1622
+ const cols = clampDimension(params.cols, 80);
1623
+ const rows = clampDimension(params.rows, 24);
1624
+ this.closeActive();
1625
+ const id = (0, import_node_crypto.randomUUID)();
1626
+ const pty = this.spawnPty({ cwd, cols, rows });
1627
+ this.session = { id, pty, cwd };
1628
+ pty.onData((data) => this.pushOutput(id, data));
1629
+ pty.onExit((info) => {
1630
+ if (this.session?.id !== id) return;
1631
+ this.flush(id);
1632
+ this.session = null;
1633
+ this.send({
1634
+ jsonrpc: "2.0",
1635
+ method: TERMINAL_EVENTS.EXIT,
1636
+ params: {
1637
+ sessionId: id,
1638
+ exitCode: info.exitCode,
1639
+ signal: info.signal
1640
+ }
1641
+ });
1642
+ });
1643
+ return { sessionId: id, cwd };
1644
+ }
1645
+ input(params) {
1646
+ const session = this.requireSession(params.sessionId);
1647
+ if (typeof params.data !== "string") {
1648
+ throw new BashInvalidParamsError("data \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1649
+ }
1650
+ if (params.data.length > MAX_INPUT) {
1651
+ throw new BashInvalidParamsError("\u5355\u6B21\u8F93\u5165\u8FC7\u957F");
1652
+ }
1653
+ session.pty.write(params.data);
1654
+ return { ok: true };
1655
+ }
1656
+ resize(params) {
1657
+ const session = this.requireSession(params.sessionId);
1658
+ session.pty.resize(clampDimension(params.cols, 80), clampDimension(params.rows, 24));
1659
+ return { ok: true };
1660
+ }
1661
+ close(params = {}) {
1662
+ if (!this.session || params.sessionId && params.sessionId !== this.session.id) {
1663
+ return { ok: true };
1664
+ }
1665
+ this.closeActive();
1666
+ return { ok: true };
1667
+ }
1668
+ disposeAll() {
1669
+ this.closeActive();
1670
+ }
1671
+ requireSession(sessionId) {
1672
+ if (!this.session || !sessionId || sessionId !== this.session.id) {
1673
+ throw new Error("\u7EC8\u7AEF\u4F1A\u8BDD\u4E0D\u5B58\u5728");
1674
+ }
1675
+ return this.session;
1676
+ }
1677
+ closeActive() {
1678
+ const current = this.session;
1679
+ this.session = null;
1680
+ this.buffer = "";
1681
+ if (this.timer) {
1682
+ clearTimeout(this.timer);
1683
+ this.timer = null;
1684
+ }
1685
+ current?.pty.kill();
1686
+ }
1687
+ pushOutput(sessionId, chunk) {
1688
+ if (this.session?.id !== sessionId || !chunk) return;
1689
+ this.buffer += chunk;
1690
+ this.flushFullChunks(sessionId);
1691
+ if (!this.buffer) return;
1692
+ if (this.coalesceMs <= 0) {
1693
+ this.flush(sessionId);
1694
+ return;
1695
+ }
1696
+ if (!this.timer) {
1697
+ this.timer = setTimeout(() => this.flush(sessionId), this.coalesceMs);
1698
+ }
1699
+ }
1700
+ flushFullChunks(sessionId) {
1701
+ while (this.buffer.length >= OUTPUT_CHUNK) {
1702
+ const data = this.buffer.slice(0, OUTPUT_CHUNK);
1703
+ this.buffer = this.buffer.slice(OUTPUT_CHUNK);
1704
+ this.emitOutput(sessionId, data);
1705
+ }
1706
+ }
1707
+ flush(sessionId) {
1708
+ if (this.timer) {
1709
+ clearTimeout(this.timer);
1710
+ this.timer = null;
1711
+ }
1712
+ if (!this.buffer || this.session?.id !== sessionId) {
1713
+ this.buffer = "";
1714
+ return;
1715
+ }
1716
+ const data = this.buffer;
1717
+ this.buffer = "";
1718
+ this.emitOutput(sessionId, data);
1719
+ }
1720
+ emitOutput(sessionId, data) {
1721
+ this.send({
1722
+ jsonrpc: "2.0",
1723
+ method: TERMINAL_EVENTS.OUTPUT,
1724
+ params: { sessionId, data }
1725
+ });
1726
+ }
1727
+ };
1307
1728
  }
1308
1729
  });
1309
1730
 
@@ -1341,7 +1762,7 @@ function resolveOrCreateAuthToken(explicitToken) {
1341
1762
  }
1342
1763
  } catch {
1343
1764
  }
1344
- const newToken = (0, import_node_crypto.randomBytes)(32).toString("hex");
1765
+ const newToken = (0, import_node_crypto2.randomBytes)(32).toString("hex");
1345
1766
  try {
1346
1767
  if (!import_node_fs3.default.existsSync(TOKEN_FILE_DIR)) {
1347
1768
  import_node_fs3.default.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
@@ -1355,10 +1776,10 @@ function parseMajorVersion(v) {
1355
1776
  return parseInt(v.split(".")[0] ?? "0", 10);
1356
1777
  }
1357
1778
  function deriveSharedSecret(tokenOrExtensionId, hmacSalt) {
1358
- return (0, import_node_crypto.createHash)("sha256").update(`${tokenOrExtensionId}:${hmacSalt}`).digest();
1779
+ return (0, import_node_crypto2.createHash)("sha256").update(`${tokenOrExtensionId}:${hmacSalt}`).digest();
1359
1780
  }
1360
1781
  function computeHmac(secret, challenge) {
1361
- return (0, import_node_crypto.createHmac)("sha256", secret).update(challenge).digest("hex");
1782
+ return (0, import_node_crypto2.createHmac)("sha256", secret).update(challenge).digest("hex");
1362
1783
  }
1363
1784
  function verifyHmacConstantTime(actualHex, expectedHex) {
1364
1785
  if (!actualHex || typeof actualHex !== "string") return false;
@@ -1366,7 +1787,7 @@ function verifyHmacConstantTime(actualHex, expectedHex) {
1366
1787
  const actualBuf = Buffer.from(actualHex, "hex");
1367
1788
  const expectedBuf = Buffer.from(expectedHex, "hex");
1368
1789
  if (actualBuf.length !== expectedBuf.length) return false;
1369
- return (0, import_node_crypto.timingSafeEqual)(actualBuf, expectedBuf);
1790
+ return (0, import_node_crypto2.timingSafeEqual)(actualBuf, expectedBuf);
1370
1791
  } catch {
1371
1792
  return false;
1372
1793
  }
@@ -1377,19 +1798,20 @@ function getKillPortHint(port) {
1377
1798
  }
1378
1799
  return `lsof -ti:${port} | xargs kill -9`;
1379
1800
  }
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;
1801
+ var import_ws, import_node_events, import_node_crypto2, 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;
1381
1802
  var init_bridge_client = __esm({
1382
1803
  "src/bridge/bridge-client.ts"() {
1383
1804
  "use strict";
1384
1805
  import_ws = require("ws");
1385
1806
  import_node_events = require("events");
1386
- import_node_crypto = require("crypto");
1807
+ import_node_crypto2 = require("crypto");
1387
1808
  import_node_fs3 = __toESM(require("fs"), 1);
1388
1809
  import_node_path3 = __toESM(require("path"), 1);
1389
1810
  import_node_os2 = __toESM(require("os"), 1);
1390
1811
  init_protocol();
1391
1812
  init_bash_executor();
1392
1813
  init_directory_picker();
1814
+ init_terminal_session();
1393
1815
  WS_PORT = DEFAULT_WS_PORT;
1394
1816
  REQUEST_TIMEOUT_MS = 3e4;
1395
1817
  HANDSHAKE_TIMEOUT_MS = 5e3;
@@ -1411,6 +1833,7 @@ var init_bridge_client = __esm({
1411
1833
  pending = /* @__PURE__ */ new Map();
1412
1834
  socketAuthStates = /* @__PURE__ */ new Map();
1413
1835
  destroyed = false;
1836
+ terminalSessions = null;
1414
1837
  idCounter = 0;
1415
1838
  isClientMode = false;
1416
1839
  explicitAuthToken;
@@ -1665,8 +2088,8 @@ var init_bridge_client = __esm({
1665
2088
  this.emit("error", err);
1666
2089
  });
1667
2090
  this.wss.on("connection", (socket) => {
1668
- const challenge = (0, import_node_crypto.randomUUID)();
1669
- const hmacSalt = (0, import_node_crypto.randomBytes)(16).toString("hex");
2091
+ const challenge = (0, import_node_crypto2.randomUUID)();
2092
+ const hmacSalt = (0, import_node_crypto2.randomBytes)(16).toString("hex");
1670
2093
  const authState = {
1671
2094
  authenticated: false,
1672
2095
  challenge,
@@ -1707,6 +2130,7 @@ var init_bridge_client = __esm({
1707
2130
  }
1708
2131
  if (socket === this.extensionSocket) {
1709
2132
  this.extensionSocket = null;
2133
+ this.terminalSessions?.disposeAll();
1710
2134
  this.emit("disconnected");
1711
2135
  for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1712
2136
  if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
@@ -1753,6 +2177,16 @@ var init_bridge_client = __esm({
1753
2177
  return;
1754
2178
  }
1755
2179
  if (msg.method && msg.id) {
2180
+ if (typeof msg.method === "string" && isTerminalMethod(msg.method) && socket !== this.extensionSocket) {
2181
+ this.sendJsonRpcResponse(socket, {
2182
+ id: msg.id,
2183
+ error: {
2184
+ code: BRIDGE_ERROR_CODE.METHOD_NOT_FOUND,
2185
+ message: "\u7EC8\u7AEF\u4F1A\u8BDD\u4EC5\u5141\u8BB8\u5DF2\u8BA4\u8BC1\u7684\u6269\u5C55\u8FDE\u63A5"
2186
+ }
2187
+ });
2188
+ return;
2189
+ }
1756
2190
  if (socket === this.extensionSocket) {
1757
2191
  await this.handleExtensionSystemRpc(socket, msg);
1758
2192
  return;
@@ -1916,9 +2350,6 @@ var init_bridge_client = __esm({
1916
2350
  systemRpcHandlers = Object.assign(
1917
2351
  /* @__PURE__ */ Object.create(null),
1918
2352
  {
1919
- [BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
1920
- return executeBash(params || {});
1921
- },
1922
2353
  [BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
1923
2354
  const selectedPath = await pickDirectory({
1924
2355
  title: params?.title || void 0,
@@ -1929,11 +2360,43 @@ var init_bridge_client = __esm({
1929
2360
  selectedPath,
1930
2361
  canceled: selectedPath === null
1931
2362
  };
1932
- }
2363
+ },
2364
+ [BRIDGE_METHODS.TERMINAL_OPEN]: async (params) => this.getTerminalSessions().open(params || {}),
2365
+ [BRIDGE_METHODS.TERMINAL_INPUT]: async (params) => this.getTerminalSessions().input(params || {}),
2366
+ [BRIDGE_METHODS.TERMINAL_RESIZE]: async (params) => this.getTerminalSessions().resize(params || {}),
2367
+ [BRIDGE_METHODS.TERMINAL_CLOSE]: async (params) => this.getTerminalSessions().close(params || {})
1933
2368
  }
1934
2369
  );
2370
+ sendJsonRpcNotification(socket, method, params) {
2371
+ if (socket.readyState === WS_OPEN) {
2372
+ socket.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
2373
+ }
2374
+ }
2375
+ async executeHostBash(socket, id, params) {
2376
+ try {
2377
+ const requestParams = params && typeof params === "object" ? params : {};
2378
+ const result = await executeBash({
2379
+ ...requestParams,
2380
+ onOutput: (chunk) => {
2381
+ if (!chunk.text) return;
2382
+ this.sendJsonRpcNotification(socket, BASH_EVENTS.OUTPUT, {
2383
+ id,
2384
+ stream: chunk.stream,
2385
+ text: chunk.text
2386
+ });
2387
+ }
2388
+ });
2389
+ this.sendJsonRpcResponse(socket, { id, result });
2390
+ } catch (err) {
2391
+ this.sendJsonRpcResponse(socket, { id, error: this.formatSystemRpcError(err) });
2392
+ }
2393
+ }
1935
2394
  async handleExtensionSystemRpc(socket, msg) {
1936
2395
  const { id, method, params } = msg;
2396
+ if (method === BRIDGE_METHODS.SYSTEM_BASH) {
2397
+ await this.executeHostBash(socket, id, params);
2398
+ return;
2399
+ }
1937
2400
  const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
1938
2401
  if (!handler || typeof handler !== "function") {
1939
2402
  this.sendJsonRpcResponse(socket, {
@@ -2029,9 +2492,26 @@ var init_bridge_client = __esm({
2029
2492
  }
2030
2493
  });
2031
2494
  }
2495
+ getTerminalSessions() {
2496
+ if (!this.terminalSessions) {
2497
+ this.terminalSessions = new TerminalSessionManager({
2498
+ send: (frame) => {
2499
+ const socket = this.extensionSocket;
2500
+ if (socket && socket.readyState === WS_OPEN) {
2501
+ try {
2502
+ socket.send(JSON.stringify(frame));
2503
+ } catch {
2504
+ }
2505
+ }
2506
+ }
2507
+ });
2508
+ }
2509
+ return this.terminalSessions;
2510
+ }
2032
2511
  /** 销毁服务,断开所有连接 */
2033
2512
  destroy() {
2034
2513
  this.destroyed = true;
2514
+ this.terminalSessions?.disposeAll();
2035
2515
  this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
2036
2516
  this.forwardedRequests.clear();
2037
2517
  if (this.clientSocket) {
@@ -2176,7 +2656,7 @@ __export(daemon_exports, {
2176
2656
  isDaemonProcessAlive: () => isDaemonProcessAlive,
2177
2657
  isProcessAlive: () => isProcessAlive,
2178
2658
  killProcessOnPort: () => killProcessOnPort,
2179
- killProcessTree: () => killProcessTree2,
2659
+ killProcessTree: () => killProcessTree,
2180
2660
  probeServerReady: () => probeServerReady,
2181
2661
  runDaemonProcess: () => runDaemonProcess,
2182
2662
  stopBridgeDaemon: () => stopBridgeDaemon
@@ -2229,7 +2709,7 @@ async function findPidByPort(port, excludePid) {
2229
2709
  return null;
2230
2710
  }
2231
2711
  }
2232
- async function killProcessTree2(pid) {
2712
+ async function killProcessTree(pid) {
2233
2713
  if (!pid || pid <= 0 || pid === process.pid) return false;
2234
2714
  if (process.platform === "win32") {
2235
2715
  try {
@@ -2267,7 +2747,7 @@ async function killProcessTree2(pid) {
2267
2747
  async function killProcessOnPort(port) {
2268
2748
  const pid = await findPidByPort(port, process.pid);
2269
2749
  if (pid) {
2270
- await killProcessTree2(pid);
2750
+ await killProcessTree(pid);
2271
2751
  for (let i = 0; i < 20; i++) {
2272
2752
  await new Promise((r) => setTimeout(r, 100));
2273
2753
  const remainingPid = await findPidByPort(port, process.pid);
@@ -2449,7 +2929,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2449
2929
  if (portPid && portPid !== daemonPid) {
2450
2930
  return false;
2451
2931
  }
2452
- const killed = await killProcessTree2(daemonPid);
2932
+ const killed = await killProcessTree(daemonPid);
2453
2933
  if (killed) {
2454
2934
  killedAny = true;
2455
2935
  try {
@@ -2460,7 +2940,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2460
2940
  } else {
2461
2941
  if (daemonPid) {
2462
2942
  try {
2463
- const killed = await killProcessTree2(daemonPid);
2943
+ const killed = await killProcessTree(daemonPid);
2464
2944
  if (killed) {
2465
2945
  killedAny = true;
2466
2946
  try {