@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.js CHANGED
@@ -526,9 +526,17 @@ var init_package = __esm({
526
526
  "package.json"() {
527
527
  package_default = {
528
528
  name: "@opentiny/webmcp-cli",
529
- version: "0.1.0-alpha.1",
529
+ version: "0.1.0-beta.1",
530
530
  type: "module",
531
531
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
532
+ repository: {
533
+ type: "git",
534
+ url: "git+https://github.com/opentiny/webmcp-sdk.git",
535
+ directory: "packages/webmcp-cli"
536
+ },
537
+ bugs: {
538
+ url: "https://github.com/opentiny/webmcp-sdk/issues"
539
+ },
532
540
  main: "dist/index.js",
533
541
  types: "dist/index.d.ts",
534
542
  bin: {
@@ -557,7 +565,7 @@ var init_package = __esm({
557
565
  license: "MIT",
558
566
  author: "OpenTiny Team",
559
567
  engines: {
560
- node: ">=16"
568
+ node: ">=18.19.0"
561
569
  },
562
570
  publishConfig: {
563
571
  access: "public"
@@ -569,6 +577,7 @@ var init_package = __esm({
569
577
  "@page-agent/page-controller": "catalog:",
570
578
  clipboardy: "^5.3.1",
571
579
  commander: "^11.1.0",
580
+ execa: "^9.6.1",
572
581
  json5: "^2.2.3",
573
582
  marked: "^15.0.7",
574
583
  picocolors: "^1.0.0",
@@ -797,12 +806,15 @@ var init_cdp_adapter = __esm({
797
806
  });
798
807
 
799
808
  // src/bridge/protocol.ts
800
- var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS;
809
+ function isTerminalMethod(method) {
810
+ return TERMINAL_METHOD_SET.has(method);
811
+ }
812
+ var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS, TERMINAL_EVENTS, BASH_EVENTS, TERMINAL_METHOD_SET;
801
813
  var init_protocol = __esm({
802
814
  "src/bridge/protocol.ts"() {
803
815
  "use strict";
804
816
  DEFAULT_WS_PORT = 18999;
805
- PROTOCOL_VERSION = "1.1.0";
817
+ PROTOCOL_VERSION = "1.2.0";
806
818
  WS_CLOSE_CODE = {
807
819
  /** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
808
820
  REPLACED: 4e3,
@@ -836,8 +848,25 @@ var init_protocol = __esm({
836
848
  SKILLS_GET: "skills/get",
837
849
  SKILLS_READ_RESOURCE: "skills/read_resource",
838
850
  SYSTEM_BASH: "system/bash",
839
- SYSTEM_PICK_DIRECTORY: "system/pick_directory"
851
+ SYSTEM_PICK_DIRECTORY: "system/pick_directory",
852
+ TERMINAL_OPEN: "terminal/open",
853
+ TERMINAL_INPUT: "terminal/input",
854
+ TERMINAL_RESIZE: "terminal/resize",
855
+ TERMINAL_CLOSE: "terminal/close"
840
856
  };
857
+ TERMINAL_EVENTS = {
858
+ OUTPUT: "notification/terminal_output",
859
+ EXIT: "notification/terminal_exit"
860
+ };
861
+ BASH_EVENTS = {
862
+ OUTPUT: "notification/bash_output"
863
+ };
864
+ TERMINAL_METHOD_SET = /* @__PURE__ */ new Set([
865
+ BRIDGE_METHODS.TERMINAL_OPEN,
866
+ BRIDGE_METHODS.TERMINAL_INPUT,
867
+ BRIDGE_METHODS.TERMINAL_RESIZE,
868
+ BRIDGE_METHODS.TERMINAL_CLOSE
869
+ ]);
841
870
  }
842
871
  });
843
872
 
@@ -898,7 +927,8 @@ var init_workspace_manager = __esm({
898
927
  // src/bridge/bash-executor.ts
899
928
  import fs3 from "fs";
900
929
  import path3 from "path";
901
- import { spawn as spawn2 } from "child_process";
930
+ import { StringDecoder } from "string_decoder";
931
+ import { execa } from "execa";
902
932
  function findWindowsBash() {
903
933
  const pathDirs = (process.env.PATH || "").split(path3.delimiter);
904
934
  for (const dir of pathDirs) {
@@ -949,25 +979,56 @@ function resolveShell(forcePlatform) {
949
979
  if (!forcePlatform) cachedShell = fallback;
950
980
  return fallback;
951
981
  }
952
- function killProcessTree(child) {
953
- if (process.platform === "win32" && child.pid) {
954
- try {
955
- spawn2("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
956
- windowsHide: true,
957
- stdio: "ignore"
958
- });
959
- } catch {
960
- child.kill("SIGKILL");
982
+ function withoutCarriageReturn(buf) {
983
+ if (!buf.includes(13)) return buf;
984
+ const out = Buffer.allocUnsafe(buf.length);
985
+ let size = 0;
986
+ for (let index = 0; index < buf.length; index += 1) {
987
+ const byte = buf[index];
988
+ if (byte === 13) continue;
989
+ out[size] = byte;
990
+ size += 1;
991
+ }
992
+ return out.subarray(0, size);
993
+ }
994
+ function collectLimited(stream, maxBuffer, onText, stripCarriageReturn = false) {
995
+ const state = { chunks: [], truncated: false };
996
+ if (!stream) return state;
997
+ const decoder = new StringDecoder("utf8");
998
+ let bytes = 0;
999
+ const emit = (buf) => {
1000
+ const text = decoder.write(buf);
1001
+ if (text) onText?.(text);
1002
+ };
1003
+ stream.on("data", (chunk) => {
1004
+ const raw = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
1005
+ const buf = stripCarriageReturn ? withoutCarriageReturn(raw) : raw;
1006
+ if (buf.length === 0) return;
1007
+ if (bytes >= maxBuffer) {
1008
+ state.truncated = true;
1009
+ return;
961
1010
  }
962
- } else {
963
- child.kill("SIGTERM");
964
- setTimeout(() => {
965
- try {
966
- child.kill("SIGKILL");
967
- } catch {
968
- }
969
- }, 2e3);
970
- }
1011
+ const remaining = maxBuffer - bytes;
1012
+ if (buf.length <= remaining) {
1013
+ state.chunks.push(buf);
1014
+ bytes += buf.length;
1015
+ emit(buf);
1016
+ return;
1017
+ }
1018
+ const kept = buf.subarray(0, remaining);
1019
+ state.chunks.push(kept);
1020
+ bytes += remaining;
1021
+ state.truncated = true;
1022
+ emit(kept);
1023
+ });
1024
+ stream.on("end", () => {
1025
+ const rest = decoder.end();
1026
+ if (rest) onText?.(rest);
1027
+ });
1028
+ return state;
1029
+ }
1030
+ function decodeLimited(state) {
1031
+ return Buffer.concat(state.chunks).toString("utf8").trim();
971
1032
  }
972
1033
  async function executeBash(params) {
973
1034
  const trimmedCommand = params.command?.trim();
@@ -986,77 +1047,59 @@ async function executeBash(params) {
986
1047
  });
987
1048
  const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
988
1049
  const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
989
- const startTime = Date.now();
990
1050
  const shell = resolveShell();
991
- return new Promise((resolve3, reject) => {
992
- const stdoutChunks = [];
993
- let stdoutBytes = 0;
994
- const stderrChunks = [];
995
- let stderrBytes = 0;
996
- let truncated = false;
997
- let isKilled = false;
998
- const child = spawn2(trimmedCommand, {
999
- shell,
1000
- cwd,
1001
- env: { ...process.env, ...env },
1002
- windowsHide: true
1003
- });
1004
- const killTimeoutTimer = setTimeout(() => {
1005
- isKilled = true;
1006
- killProcessTree(child);
1007
- reject(new BashTimeoutError(normalizedTimeoutMs));
1008
- }, normalizedTimeoutMs);
1009
- child.stdout.on("data", (chunk) => {
1010
- if (stdoutBytes < maxBuffer) {
1011
- const remaining = maxBuffer - stdoutBytes;
1012
- if (chunk.length <= remaining) {
1013
- stdoutChunks.push(chunk);
1014
- stdoutBytes += chunk.length;
1015
- } else {
1016
- stdoutChunks.push(chunk.subarray(0, remaining));
1017
- stdoutBytes += remaining;
1018
- truncated = true;
1019
- }
1020
- } else {
1021
- truncated = true;
1022
- }
1023
- });
1024
- child.stderr.on("data", (chunk) => {
1025
- if (stderrBytes < maxBuffer) {
1026
- const remaining = maxBuffer - stderrBytes;
1027
- if (chunk.length <= remaining) {
1028
- stderrChunks.push(chunk);
1029
- stderrBytes += chunk.length;
1030
- } else {
1031
- stderrChunks.push(chunk.subarray(0, remaining));
1032
- stderrBytes += remaining;
1033
- truncated = true;
1034
- }
1035
- } else {
1036
- truncated = true;
1037
- }
1038
- });
1039
- child.on("error", (err) => {
1040
- clearTimeout(killTimeoutTimer);
1041
- reject(err);
1042
- });
1043
- child.on("close", (exitCode, signal) => {
1044
- clearTimeout(killTimeoutTimer);
1045
- if (isKilled) return;
1046
- const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
1047
- const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
1048
- resolve3({
1049
- stdout,
1050
- stderr,
1051
- exitCode,
1052
- signal,
1053
- durationMs: Date.now() - startTime,
1054
- ...truncated ? { truncated: true } : {}
1055
- });
1056
- });
1051
+ const useTty = params.tty === true && process.platform !== "win32";
1052
+ const subprocess = useTty ? execa("python3", ["-u", "-c", TTY_HELPER], {
1053
+ cwd,
1054
+ env: {
1055
+ ...env,
1056
+ BASH_TTY_COMMAND: trimmedCommand,
1057
+ BASH_TTY_SHELL: shell
1058
+ },
1059
+ stdin: "ignore",
1060
+ buffer: false,
1061
+ encoding: "buffer",
1062
+ reject: false,
1063
+ timeout: normalizedTimeoutMs,
1064
+ forceKillAfterDelay: FORCE_KILL_AFTER_DELAY_MS,
1065
+ windowsHide: true,
1066
+ stripFinalNewline: false
1067
+ }) : execa(trimmedCommand, {
1068
+ shell,
1069
+ cwd,
1070
+ env,
1071
+ stdin: "ignore",
1072
+ buffer: false,
1073
+ encoding: "buffer",
1074
+ reject: false,
1075
+ timeout: normalizedTimeoutMs,
1076
+ forceKillAfterDelay: FORCE_KILL_AFTER_DELAY_MS,
1077
+ windowsHide: true,
1078
+ stripFinalNewline: false
1057
1079
  });
1080
+ const emit = (stream) => (text) => {
1081
+ params.onOutput?.({ stream, text });
1082
+ };
1083
+ const stdoutState = collectLimited(subprocess.stdout, maxBuffer, emit("stdout"), useTty);
1084
+ const stderrState = collectLimited(subprocess.stderr, maxBuffer, emit("stderr"));
1085
+ const result = await subprocess;
1086
+ if (result.timedOut) {
1087
+ throw new BashTimeoutError(normalizedTimeoutMs);
1088
+ }
1089
+ if (result.failed && result.exitCode == null && result.signal == null && !result.isTerminated) {
1090
+ throw new Error(result.shortMessage || result.message || "\u547D\u4EE4\u542F\u52A8\u5931\u8D25");
1091
+ }
1092
+ const truncated = stdoutState.truncated || stderrState.truncated;
1093
+ return {
1094
+ stdout: decodeLimited(stdoutState),
1095
+ stderr: decodeLimited(stderrState),
1096
+ exitCode: result.exitCode ?? null,
1097
+ signal: result.signal ?? null,
1098
+ durationMs: result.durationMs ?? 0,
1099
+ ...truncated ? { truncated: true } : {}
1100
+ };
1058
1101
  }
1059
- var BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell;
1102
+ var BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell, FORCE_KILL_AFTER_DELAY_MS, TTY_HELPER;
1060
1103
  var init_bash_executor = __esm({
1061
1104
  "src/bridge/bash-executor.ts"() {
1062
1105
  "use strict";
@@ -1074,11 +1117,51 @@ var init_bash_executor = __esm({
1074
1117
  MAX_TIMEOUT_MS = 6e5;
1075
1118
  DEFAULT_MAX_BUFFER = 512 * 1024;
1076
1119
  cachedShell = null;
1120
+ FORCE_KILL_AFTER_DELAY_MS = 2e3;
1121
+ TTY_HELPER = `
1122
+ import errno, os, sys
1123
+ cmd = os.environ.get("BASH_TTY_COMMAND") or ""
1124
+ shell = os.environ.get("BASH_TTY_SHELL") or "/bin/bash"
1125
+ if not cmd:
1126
+ sys.exit(127)
1127
+ import pty
1128
+ pid, fd = pty.fork()
1129
+ if pid == 0:
1130
+ os.environ.pop("BASH_TTY_COMMAND", None)
1131
+ os.environ.pop("BASH_TTY_SHELL", None)
1132
+ os.environ["TERM"] = "xterm-256color"
1133
+ try:
1134
+ os.execvp(shell, [shell, "-c", cmd])
1135
+ except OSError:
1136
+ os._exit(127)
1137
+ while True:
1138
+ try:
1139
+ data = os.read(fd, 65536)
1140
+ except OSError as exc:
1141
+ if exc.errno == errno.EINTR:
1142
+ continue
1143
+ break
1144
+ if not data:
1145
+ break
1146
+ try:
1147
+ os.write(1, data)
1148
+ except OSError:
1149
+ break
1150
+ try:
1151
+ _, status = os.waitpid(pid, 0)
1152
+ except OSError:
1153
+ sys.exit(1)
1154
+ if os.WIFEXITED(status):
1155
+ sys.exit(os.WEXITSTATUS(status))
1156
+ if os.WIFSIGNALED(status):
1157
+ sys.exit(128 + os.WTERMSIG(status))
1158
+ sys.exit(1)
1159
+ `;
1077
1160
  }
1078
1161
  });
1079
1162
 
1080
1163
  // src/bridge/directory-picker.ts
1081
- import { spawn as spawn3 } from "child_process";
1164
+ import { spawn as spawn2 } from "child_process";
1082
1165
  async function pickDirectory(optionsOrTitle) {
1083
1166
  const options = typeof optionsOrTitle === "string" ? { title: optionsOrTitle } : optionsOrTitle || {};
1084
1167
  const title = options.title || "\u8BF7\u9009\u62E9\u5DE5\u4F5C\u7A7A\u95F4\u6587\u4EF6\u5939";
@@ -1097,7 +1180,7 @@ function pickDirectoryMac(title, defaultPath) {
1097
1180
  const defaultClause = defaultPath ? ` default location POSIX file "${defaultPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : "";
1098
1181
  const appleScript = `POSIX path of (choose folder with prompt "${safeTitle}"${defaultClause})`;
1099
1182
  return new Promise((resolve3, reject) => {
1100
- const child = spawn3("osascript", ["-e", appleScript], {
1183
+ const child = spawn2("osascript", ["-e", appleScript], {
1101
1184
  stdio: ["ignore", "pipe", "pipe"]
1102
1185
  });
1103
1186
  let stdout = "";
@@ -1215,7 +1298,7 @@ $result = [FolderPicker]::Pick("${safeTitle}", "${safeDefaultPath}")
1215
1298
  if ($result) { Write-Output $result }
1216
1299
  `;
1217
1300
  return new Promise((resolve3, reject) => {
1218
- const child = spawn3("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1301
+ const child = spawn2("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1219
1302
  stdio: ["ignore", "pipe", "pipe"],
1220
1303
  windowsHide: true
1221
1304
  });
@@ -1246,7 +1329,7 @@ function pickDirectoryLinux(title, defaultPath) {
1246
1329
  args.push(`--filename=${defaultPath}`);
1247
1330
  }
1248
1331
  return new Promise((resolve3, reject) => {
1249
- const child = spawn3("zenity", args, {
1332
+ const child = spawn2("zenity", args, {
1250
1333
  stdio: ["ignore", "pipe", "pipe"]
1251
1334
  });
1252
1335
  let stdout = "";
@@ -1284,6 +1367,344 @@ var init_directory_picker = __esm({
1284
1367
  }
1285
1368
  });
1286
1369
 
1370
+ // src/bridge/pty-backend.ts
1371
+ import { spawn as spawn3 } from "child_process";
1372
+ function spawnHostPty(options) {
1373
+ if (process.platform === "win32") {
1374
+ throw new Error("\u5F53\u524D\u5E73\u53F0\u6682\u4E0D\u652F\u6301\u4EA4\u4E92\u5F0F\u7EC8\u7AEF");
1375
+ }
1376
+ const shell = options.shell || resolveShell();
1377
+ const child = spawn3("python3", ["-u", "-c", PTY_HELPER], {
1378
+ cwd: options.cwd,
1379
+ env: {
1380
+ ...process.env,
1381
+ TERM: "xterm-256color",
1382
+ TERMINAL_SHELL: shell,
1383
+ TERMINAL_ROWS: String(options.rows),
1384
+ TERMINAL_COLS: String(options.cols)
1385
+ },
1386
+ stdio: ["pipe", "pipe", "pipe", "pipe"]
1387
+ });
1388
+ const pending = [];
1389
+ let dataCb = null;
1390
+ let exitCb = null;
1391
+ let pendingExit = null;
1392
+ let settled = false;
1393
+ const decoder = new TextDecoder("utf-8", { fatal: false });
1394
+ let stderrText = "";
1395
+ const emitData = (data) => {
1396
+ if (!data) return;
1397
+ if (dataCb) dataCb(data);
1398
+ else pending.push(data);
1399
+ };
1400
+ const finish = (info) => {
1401
+ if (settled) return;
1402
+ settled = true;
1403
+ if (stderrText.trim()) emitData(`\r
1404
+ ${stderrText.trim()}\r
1405
+ `);
1406
+ if (exitCb) exitCb(info);
1407
+ else pendingExit = info;
1408
+ };
1409
+ child.stdout.on("data", (buf) => {
1410
+ emitData(decoder.decode(buf, { stream: true }));
1411
+ });
1412
+ child.stderr.on("data", (buf) => {
1413
+ stderrText += buf.toString("utf8");
1414
+ });
1415
+ child.on("error", (err) => {
1416
+ emitData(`\r
1417
+ [\u7EC8\u7AEF\u542F\u52A8\u5931\u8D25] ${err.message}\r
1418
+ `);
1419
+ finish({ exitCode: null, signal: null });
1420
+ });
1421
+ child.on("exit", (code, signal) => {
1422
+ const rest = decoder.decode();
1423
+ if (rest) emitData(rest);
1424
+ finish({ exitCode: code, signal: signal ?? null });
1425
+ });
1426
+ const control = child.stdio[3];
1427
+ const writeControl = (payload) => {
1428
+ if (!control || !("write" in control)) return;
1429
+ try {
1430
+ ;
1431
+ control.write(`${JSON.stringify(payload)}
1432
+ `);
1433
+ } catch {
1434
+ }
1435
+ };
1436
+ return {
1437
+ write(data) {
1438
+ try {
1439
+ child.stdin.write(data);
1440
+ } catch {
1441
+ }
1442
+ },
1443
+ resize(cols, rows) {
1444
+ writeControl({ type: "resize", cols, rows });
1445
+ },
1446
+ kill() {
1447
+ writeControl({ type: "kill" });
1448
+ try {
1449
+ child.kill("SIGTERM");
1450
+ } catch {
1451
+ }
1452
+ },
1453
+ onData(cb) {
1454
+ dataCb = cb;
1455
+ for (const chunk of pending) cb(chunk);
1456
+ pending.length = 0;
1457
+ },
1458
+ onExit(cb) {
1459
+ exitCb = cb;
1460
+ if (pendingExit) cb(pendingExit);
1461
+ }
1462
+ };
1463
+ }
1464
+ var PTY_HELPER;
1465
+ var init_pty_backend = __esm({
1466
+ "src/bridge/pty-backend.ts"() {
1467
+ "use strict";
1468
+ init_bash_executor();
1469
+ PTY_HELPER = `
1470
+ import errno, fcntl, json, os, pty, select, signal, struct, sys, termios
1471
+
1472
+ shell = os.environ.get("TERMINAL_SHELL") or "/bin/bash"
1473
+ rows = int(os.environ.get("TERMINAL_ROWS") or "24")
1474
+ cols = int(os.environ.get("TERMINAL_COLS") or "80")
1475
+ control_fd = 3
1476
+
1477
+ def set_winsize(fd, next_rows, next_cols):
1478
+ winsize = struct.pack("HHHH", int(next_rows), int(next_cols), 0, 0)
1479
+ fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize)
1480
+
1481
+ pid, master = pty.fork()
1482
+ if pid == 0:
1483
+ try:
1484
+ os.close(control_fd)
1485
+ except OSError:
1486
+ pass
1487
+ os.environ["TERM"] = "xterm-256color"
1488
+ try:
1489
+ os.execvp(shell, [shell, "-i"])
1490
+ except OSError:
1491
+ os._exit(127)
1492
+
1493
+ def control_loop():
1494
+ try:
1495
+ reader = os.fdopen(control_fd, "r", buffering=1, encoding="utf-8", errors="replace")
1496
+ except OSError:
1497
+ return
1498
+ for line in reader:
1499
+ text = line.strip()
1500
+ if not text:
1501
+ continue
1502
+ try:
1503
+ msg = json.loads(text)
1504
+ except json.JSONDecodeError:
1505
+ continue
1506
+ kind = msg.get("type")
1507
+ if kind == "resize":
1508
+ try:
1509
+ set_winsize(master, msg.get("rows") or rows, msg.get("cols") or cols)
1510
+ os.kill(pid, signal.SIGWINCH)
1511
+ except OSError:
1512
+ pass
1513
+ elif kind == "kill":
1514
+ try:
1515
+ os.kill(pid, signal.SIGHUP)
1516
+ except OSError:
1517
+ pass
1518
+ os._exit(0)
1519
+
1520
+ import threading
1521
+ threading.Thread(target=control_loop, daemon=True).start()
1522
+
1523
+ try:
1524
+ set_winsize(master, rows, cols)
1525
+ except OSError:
1526
+ pass
1527
+
1528
+ stdin_fd = sys.stdin.fileno()
1529
+ stdout_fd = sys.stdout.fileno()
1530
+
1531
+ while True:
1532
+ try:
1533
+ readable, _, _ = select.select([master, stdin_fd], [], [])
1534
+ except InterruptedError:
1535
+ continue
1536
+ if master in readable:
1537
+ try:
1538
+ data = os.read(master, 65536)
1539
+ except OSError as err:
1540
+ if err.errno == errno.EIO:
1541
+ break
1542
+ raise
1543
+ if not data:
1544
+ break
1545
+ os.write(stdout_fd, data)
1546
+ if stdin_fd in readable:
1547
+ try:
1548
+ data = os.read(stdin_fd, 65536)
1549
+ except OSError:
1550
+ break
1551
+ if not data:
1552
+ break
1553
+ try:
1554
+ os.write(master, data)
1555
+ except OSError:
1556
+ break
1557
+
1558
+ try:
1559
+ _, status = os.waitpid(pid, 0)
1560
+ except ChildProcessError:
1561
+ status = 0
1562
+ code = os.waitstatus_to_exitcode(status)
1563
+ os._exit(0 if code < 0 else code)
1564
+ `;
1565
+ }
1566
+ });
1567
+
1568
+ // src/bridge/terminal-session.ts
1569
+ import { randomUUID } from "crypto";
1570
+ function clampDimension(value, fallback) {
1571
+ const parsed = typeof value === "number" ? value : Number(value);
1572
+ if (!Number.isFinite(parsed)) return fallback;
1573
+ return Math.max(2, Math.min(500, Math.floor(parsed)));
1574
+ }
1575
+ var OUTPUT_CHUNK, MAX_INPUT, DEFAULT_COALESCE_MS, TerminalSessionManager;
1576
+ var init_terminal_session = __esm({
1577
+ "src/bridge/terminal-session.ts"() {
1578
+ "use strict";
1579
+ init_workspace_manager();
1580
+ init_pty_backend();
1581
+ init_protocol();
1582
+ OUTPUT_CHUNK = 32 * 1024;
1583
+ MAX_INPUT = 64 * 1024;
1584
+ DEFAULT_COALESCE_MS = 16;
1585
+ TerminalSessionManager = class {
1586
+ session = null;
1587
+ buffer = "";
1588
+ timer = null;
1589
+ send;
1590
+ spawnPty;
1591
+ coalesceMs;
1592
+ constructor(options) {
1593
+ this.send = options.send;
1594
+ this.spawnPty = options.spawnPty ?? ((opts) => spawnHostPty(opts));
1595
+ this.coalesceMs = options.coalesceMs ?? DEFAULT_COALESCE_MS;
1596
+ }
1597
+ open(params = {}) {
1598
+ const cwd = resolveWorkspaceDir(params);
1599
+ const cols = clampDimension(params.cols, 80);
1600
+ const rows = clampDimension(params.rows, 24);
1601
+ this.closeActive();
1602
+ const id = randomUUID();
1603
+ const pty = this.spawnPty({ cwd, cols, rows });
1604
+ this.session = { id, pty, cwd };
1605
+ pty.onData((data) => this.pushOutput(id, data));
1606
+ pty.onExit((info) => {
1607
+ if (this.session?.id !== id) return;
1608
+ this.flush(id);
1609
+ this.session = null;
1610
+ this.send({
1611
+ jsonrpc: "2.0",
1612
+ method: TERMINAL_EVENTS.EXIT,
1613
+ params: {
1614
+ sessionId: id,
1615
+ exitCode: info.exitCode,
1616
+ signal: info.signal
1617
+ }
1618
+ });
1619
+ });
1620
+ return { sessionId: id, cwd };
1621
+ }
1622
+ input(params) {
1623
+ const session = this.requireSession(params.sessionId);
1624
+ if (typeof params.data !== "string") {
1625
+ throw new BashInvalidParamsError("data \u5FC5\u987B\u662F\u5B57\u7B26\u4E32");
1626
+ }
1627
+ if (params.data.length > MAX_INPUT) {
1628
+ throw new BashInvalidParamsError("\u5355\u6B21\u8F93\u5165\u8FC7\u957F");
1629
+ }
1630
+ session.pty.write(params.data);
1631
+ return { ok: true };
1632
+ }
1633
+ resize(params) {
1634
+ const session = this.requireSession(params.sessionId);
1635
+ session.pty.resize(clampDimension(params.cols, 80), clampDimension(params.rows, 24));
1636
+ return { ok: true };
1637
+ }
1638
+ close(params = {}) {
1639
+ if (!this.session || params.sessionId && params.sessionId !== this.session.id) {
1640
+ return { ok: true };
1641
+ }
1642
+ this.closeActive();
1643
+ return { ok: true };
1644
+ }
1645
+ disposeAll() {
1646
+ this.closeActive();
1647
+ }
1648
+ requireSession(sessionId) {
1649
+ if (!this.session || !sessionId || sessionId !== this.session.id) {
1650
+ throw new Error("\u7EC8\u7AEF\u4F1A\u8BDD\u4E0D\u5B58\u5728");
1651
+ }
1652
+ return this.session;
1653
+ }
1654
+ closeActive() {
1655
+ const current = this.session;
1656
+ this.session = null;
1657
+ this.buffer = "";
1658
+ if (this.timer) {
1659
+ clearTimeout(this.timer);
1660
+ this.timer = null;
1661
+ }
1662
+ current?.pty.kill();
1663
+ }
1664
+ pushOutput(sessionId, chunk) {
1665
+ if (this.session?.id !== sessionId || !chunk) return;
1666
+ this.buffer += chunk;
1667
+ this.flushFullChunks(sessionId);
1668
+ if (!this.buffer) return;
1669
+ if (this.coalesceMs <= 0) {
1670
+ this.flush(sessionId);
1671
+ return;
1672
+ }
1673
+ if (!this.timer) {
1674
+ this.timer = setTimeout(() => this.flush(sessionId), this.coalesceMs);
1675
+ }
1676
+ }
1677
+ flushFullChunks(sessionId) {
1678
+ while (this.buffer.length >= OUTPUT_CHUNK) {
1679
+ const data = this.buffer.slice(0, OUTPUT_CHUNK);
1680
+ this.buffer = this.buffer.slice(OUTPUT_CHUNK);
1681
+ this.emitOutput(sessionId, data);
1682
+ }
1683
+ }
1684
+ flush(sessionId) {
1685
+ if (this.timer) {
1686
+ clearTimeout(this.timer);
1687
+ this.timer = null;
1688
+ }
1689
+ if (!this.buffer || this.session?.id !== sessionId) {
1690
+ this.buffer = "";
1691
+ return;
1692
+ }
1693
+ const data = this.buffer;
1694
+ this.buffer = "";
1695
+ this.emitOutput(sessionId, data);
1696
+ }
1697
+ emitOutput(sessionId, data) {
1698
+ this.send({
1699
+ jsonrpc: "2.0",
1700
+ method: TERMINAL_EVENTS.OUTPUT,
1701
+ params: { sessionId, data }
1702
+ });
1703
+ }
1704
+ };
1705
+ }
1706
+ });
1707
+
1287
1708
  // src/bridge/bridge-client.ts
1288
1709
  var bridge_client_exports = {};
1289
1710
  __export(bridge_client_exports, {
@@ -1298,7 +1719,7 @@ __export(bridge_client_exports, {
1298
1719
  });
1299
1720
  import { WebSocketServer, WebSocket as WsClient } from "ws";
1300
1721
  import { EventEmitter } from "events";
1301
- import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
1722
+ import { randomUUID as randomUUID2, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
1302
1723
  import fs4 from "fs";
1303
1724
  import path4 from "path";
1304
1725
  import os3 from "os";
@@ -1367,6 +1788,7 @@ var init_bridge_client = __esm({
1367
1788
  init_protocol();
1368
1789
  init_bash_executor();
1369
1790
  init_directory_picker();
1791
+ init_terminal_session();
1370
1792
  WS_PORT = DEFAULT_WS_PORT;
1371
1793
  REQUEST_TIMEOUT_MS = 3e4;
1372
1794
  HANDSHAKE_TIMEOUT_MS = 5e3;
@@ -1388,6 +1810,7 @@ var init_bridge_client = __esm({
1388
1810
  pending = /* @__PURE__ */ new Map();
1389
1811
  socketAuthStates = /* @__PURE__ */ new Map();
1390
1812
  destroyed = false;
1813
+ terminalSessions = null;
1391
1814
  idCounter = 0;
1392
1815
  isClientMode = false;
1393
1816
  explicitAuthToken;
@@ -1642,7 +2065,7 @@ var init_bridge_client = __esm({
1642
2065
  this.emit("error", err);
1643
2066
  });
1644
2067
  this.wss.on("connection", (socket) => {
1645
- const challenge = randomUUID();
2068
+ const challenge = randomUUID2();
1646
2069
  const hmacSalt = randomBytes(16).toString("hex");
1647
2070
  const authState = {
1648
2071
  authenticated: false,
@@ -1684,6 +2107,7 @@ var init_bridge_client = __esm({
1684
2107
  }
1685
2108
  if (socket === this.extensionSocket) {
1686
2109
  this.extensionSocket = null;
2110
+ this.terminalSessions?.disposeAll();
1687
2111
  this.emit("disconnected");
1688
2112
  for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1689
2113
  if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
@@ -1730,6 +2154,16 @@ var init_bridge_client = __esm({
1730
2154
  return;
1731
2155
  }
1732
2156
  if (msg.method && msg.id) {
2157
+ if (typeof msg.method === "string" && isTerminalMethod(msg.method) && socket !== this.extensionSocket) {
2158
+ this.sendJsonRpcResponse(socket, {
2159
+ id: msg.id,
2160
+ error: {
2161
+ code: BRIDGE_ERROR_CODE.METHOD_NOT_FOUND,
2162
+ message: "\u7EC8\u7AEF\u4F1A\u8BDD\u4EC5\u5141\u8BB8\u5DF2\u8BA4\u8BC1\u7684\u6269\u5C55\u8FDE\u63A5"
2163
+ }
2164
+ });
2165
+ return;
2166
+ }
1733
2167
  if (socket === this.extensionSocket) {
1734
2168
  await this.handleExtensionSystemRpc(socket, msg);
1735
2169
  return;
@@ -1893,9 +2327,6 @@ var init_bridge_client = __esm({
1893
2327
  systemRpcHandlers = Object.assign(
1894
2328
  /* @__PURE__ */ Object.create(null),
1895
2329
  {
1896
- [BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
1897
- return executeBash(params || {});
1898
- },
1899
2330
  [BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
1900
2331
  const selectedPath = await pickDirectory({
1901
2332
  title: params?.title || void 0,
@@ -1906,11 +2337,43 @@ var init_bridge_client = __esm({
1906
2337
  selectedPath,
1907
2338
  canceled: selectedPath === null
1908
2339
  };
1909
- }
2340
+ },
2341
+ [BRIDGE_METHODS.TERMINAL_OPEN]: async (params) => this.getTerminalSessions().open(params || {}),
2342
+ [BRIDGE_METHODS.TERMINAL_INPUT]: async (params) => this.getTerminalSessions().input(params || {}),
2343
+ [BRIDGE_METHODS.TERMINAL_RESIZE]: async (params) => this.getTerminalSessions().resize(params || {}),
2344
+ [BRIDGE_METHODS.TERMINAL_CLOSE]: async (params) => this.getTerminalSessions().close(params || {})
1910
2345
  }
1911
2346
  );
2347
+ sendJsonRpcNotification(socket, method, params) {
2348
+ if (socket.readyState === WS_OPEN) {
2349
+ socket.send(JSON.stringify({ jsonrpc: "2.0", method, params }));
2350
+ }
2351
+ }
2352
+ async executeHostBash(socket, id, params) {
2353
+ try {
2354
+ const requestParams = params && typeof params === "object" ? params : {};
2355
+ const result = await executeBash({
2356
+ ...requestParams,
2357
+ onOutput: (chunk) => {
2358
+ if (!chunk.text) return;
2359
+ this.sendJsonRpcNotification(socket, BASH_EVENTS.OUTPUT, {
2360
+ id,
2361
+ stream: chunk.stream,
2362
+ text: chunk.text
2363
+ });
2364
+ }
2365
+ });
2366
+ this.sendJsonRpcResponse(socket, { id, result });
2367
+ } catch (err) {
2368
+ this.sendJsonRpcResponse(socket, { id, error: this.formatSystemRpcError(err) });
2369
+ }
2370
+ }
1912
2371
  async handleExtensionSystemRpc(socket, msg) {
1913
2372
  const { id, method, params } = msg;
2373
+ if (method === BRIDGE_METHODS.SYSTEM_BASH) {
2374
+ await this.executeHostBash(socket, id, params);
2375
+ return;
2376
+ }
1914
2377
  const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
1915
2378
  if (!handler || typeof handler !== "function") {
1916
2379
  this.sendJsonRpcResponse(socket, {
@@ -2006,9 +2469,26 @@ var init_bridge_client = __esm({
2006
2469
  }
2007
2470
  });
2008
2471
  }
2472
+ getTerminalSessions() {
2473
+ if (!this.terminalSessions) {
2474
+ this.terminalSessions = new TerminalSessionManager({
2475
+ send: (frame) => {
2476
+ const socket = this.extensionSocket;
2477
+ if (socket && socket.readyState === WS_OPEN) {
2478
+ try {
2479
+ socket.send(JSON.stringify(frame));
2480
+ } catch {
2481
+ }
2482
+ }
2483
+ }
2484
+ });
2485
+ }
2486
+ return this.terminalSessions;
2487
+ }
2009
2488
  /** 销毁服务,断开所有连接 */
2010
2489
  destroy() {
2011
2490
  this.destroyed = true;
2491
+ this.terminalSessions?.disposeAll();
2012
2492
  this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
2013
2493
  this.forwardedRequests.clear();
2014
2494
  if (this.clientSocket) {
@@ -2153,7 +2633,7 @@ __export(daemon_exports, {
2153
2633
  isDaemonProcessAlive: () => isDaemonProcessAlive,
2154
2634
  isProcessAlive: () => isProcessAlive,
2155
2635
  killProcessOnPort: () => killProcessOnPort,
2156
- killProcessTree: () => killProcessTree2,
2636
+ killProcessTree: () => killProcessTree,
2157
2637
  probeServerReady: () => probeServerReady,
2158
2638
  runDaemonProcess: () => runDaemonProcess,
2159
2639
  stopBridgeDaemon: () => stopBridgeDaemon
@@ -2212,7 +2692,7 @@ async function findPidByPort(port, excludePid) {
2212
2692
  return null;
2213
2693
  }
2214
2694
  }
2215
- async function killProcessTree2(pid) {
2695
+ async function killProcessTree(pid) {
2216
2696
  if (!pid || pid <= 0 || pid === process.pid) return false;
2217
2697
  if (process.platform === "win32") {
2218
2698
  try {
@@ -2250,7 +2730,7 @@ async function killProcessTree2(pid) {
2250
2730
  async function killProcessOnPort(port) {
2251
2731
  const pid = await findPidByPort(port, process.pid);
2252
2732
  if (pid) {
2253
- await killProcessTree2(pid);
2733
+ await killProcessTree(pid);
2254
2734
  for (let i = 0; i < 20; i++) {
2255
2735
  await new Promise((r) => setTimeout(r, 100));
2256
2736
  const remainingPid = await findPidByPort(port, process.pid);
@@ -2432,7 +2912,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2432
2912
  if (portPid && portPid !== daemonPid) {
2433
2913
  return false;
2434
2914
  }
2435
- const killed = await killProcessTree2(daemonPid);
2915
+ const killed = await killProcessTree(daemonPid);
2436
2916
  if (killed) {
2437
2917
  killedAny = true;
2438
2918
  try {
@@ -2443,7 +2923,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2443
2923
  } else {
2444
2924
  if (daemonPid) {
2445
2925
  try {
2446
- const killed = await killProcessTree2(daemonPid);
2926
+ const killed = await killProcessTree(daemonPid);
2447
2927
  if (killed) {
2448
2928
  killedAny = true;
2449
2929
  try {