@opentiny/webmcp-cli 0.0.9 → 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
@@ -33,6 +33,38 @@ function shouldLaunchHeadless() {
33
33
  if (process.env.WEBMCP_HEADLESS === "0") return false;
34
34
  return process.env.WEBMCP_HEADLESS === "1" || process.env.CI === "true" || process.env.CI === "1";
35
35
  }
36
+ function shouldIgnoreCertificateErrors() {
37
+ return process.env.WEBMCP_IGNORE_CERTIFICATE_ERRORS === "1";
38
+ }
39
+ function buildChromeLaunchArgs(input) {
40
+ const launchArgs = [
41
+ `--remote-debugging-port=${input.cdpPort}`,
42
+ `--user-data-dir=${input.userDataDir}`,
43
+ "--no-first-run",
44
+ "--no-default-browser-check"
45
+ ];
46
+ if (input.headless) {
47
+ launchArgs.push(
48
+ "--headless=new",
49
+ "--no-sandbox",
50
+ "--disable-setuid-sandbox",
51
+ "--disable-gpu",
52
+ "--disable-dev-shm-usage"
53
+ );
54
+ }
55
+ const ignoreCertificateErrors = input.ignoreCertificateErrors ?? shouldIgnoreCertificateErrors();
56
+ if (ignoreCertificateErrors) {
57
+ launchArgs.push("--ignore-certificate-errors");
58
+ }
59
+ return launchArgs;
60
+ }
61
+ function buildPuppeteerConnectOptions(browserBaseUrl) {
62
+ return {
63
+ browserURL: browserBaseUrl,
64
+ defaultViewport: null,
65
+ ...shouldIgnoreCertificateErrors() ? { ignoreHTTPSErrors: true } : {}
66
+ };
67
+ }
36
68
  function readActivePortFromProfile(userDataDir) {
37
69
  try {
38
70
  const content = fs.readFileSync(path.join(userDataDir, "DevToolsActivePort"), "utf-8");
@@ -127,7 +159,7 @@ async function findAvailableCdpUrl(addresses, retries = 2) {
127
159
  async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
128
160
  try {
129
161
  return await promiseWithTimeout(
130
- puppeteer.connect({ browserURL: browserBaseUrl, defaultViewport: null }),
162
+ puppeteer.connect(buildPuppeteerConnectOptions(browserBaseUrl)),
131
163
  1e4,
132
164
  `puppeteer.connect to ${browserBaseUrl} timed out`
133
165
  );
@@ -202,21 +234,11 @@ async function startBrowserInBackground() {
202
234
  fs.unlinkSync(path.join(userDataDir, "DevToolsActivePort"));
203
235
  } catch {
204
236
  }
205
- const launchArgs = [
206
- `--remote-debugging-port=${cdpPort}`,
207
- `--user-data-dir=${userDataDir}`,
208
- "--no-first-run",
209
- "--no-default-browser-check"
210
- ];
211
- if (shouldLaunchHeadless()) {
212
- launchArgs.push(
213
- "--headless=new",
214
- "--no-sandbox",
215
- "--disable-setuid-sandbox",
216
- "--disable-gpu",
217
- "--disable-dev-shm-usage"
218
- );
219
- }
237
+ const launchArgs = buildChromeLaunchArgs({
238
+ cdpPort,
239
+ userDataDir,
240
+ headless: shouldLaunchHeadless()
241
+ });
220
242
  const child = spawn(
221
243
  browserInfo.path,
222
244
  launchArgs,
@@ -504,9 +526,17 @@ var init_package = __esm({
504
526
  "package.json"() {
505
527
  package_default = {
506
528
  name: "@opentiny/webmcp-cli",
507
- version: "0.0.9",
529
+ version: "0.1.0-beta.1",
508
530
  type: "module",
509
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
+ },
510
540
  main: "dist/index.js",
511
541
  types: "dist/index.d.ts",
512
542
  bin: {
@@ -535,7 +565,7 @@ var init_package = __esm({
535
565
  license: "MIT",
536
566
  author: "OpenTiny Team",
537
567
  engines: {
538
- node: ">=16"
568
+ node: ">=18.19.0"
539
569
  },
540
570
  publishConfig: {
541
571
  access: "public"
@@ -547,6 +577,7 @@ var init_package = __esm({
547
577
  "@page-agent/page-controller": "catalog:",
548
578
  clipboardy: "^5.3.1",
549
579
  commander: "^11.1.0",
580
+ execa: "^9.6.1",
550
581
  json5: "^2.2.3",
551
582
  marked: "^15.0.7",
552
583
  picocolors: "^1.0.0",
@@ -775,12 +806,15 @@ var init_cdp_adapter = __esm({
775
806
  });
776
807
 
777
808
  // src/bridge/protocol.ts
778
- 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;
779
813
  var init_protocol = __esm({
780
814
  "src/bridge/protocol.ts"() {
781
815
  "use strict";
782
816
  DEFAULT_WS_PORT = 18999;
783
- PROTOCOL_VERSION = "1.1.0";
817
+ PROTOCOL_VERSION = "1.2.0";
784
818
  WS_CLOSE_CODE = {
785
819
  /** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
786
820
  REPLACED: 4e3,
@@ -814,8 +848,25 @@ var init_protocol = __esm({
814
848
  SKILLS_GET: "skills/get",
815
849
  SKILLS_READ_RESOURCE: "skills/read_resource",
816
850
  SYSTEM_BASH: "system/bash",
817
- 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"
856
+ };
857
+ TERMINAL_EVENTS = {
858
+ OUTPUT: "notification/terminal_output",
859
+ EXIT: "notification/terminal_exit"
818
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
+ ]);
819
870
  }
820
871
  });
821
872
 
@@ -876,7 +927,8 @@ var init_workspace_manager = __esm({
876
927
  // src/bridge/bash-executor.ts
877
928
  import fs3 from "fs";
878
929
  import path3 from "path";
879
- import { spawn as spawn2 } from "child_process";
930
+ import { StringDecoder } from "string_decoder";
931
+ import { execa } from "execa";
880
932
  function findWindowsBash() {
881
933
  const pathDirs = (process.env.PATH || "").split(path3.delimiter);
882
934
  for (const dir of pathDirs) {
@@ -927,25 +979,56 @@ function resolveShell(forcePlatform) {
927
979
  if (!forcePlatform) cachedShell = fallback;
928
980
  return fallback;
929
981
  }
930
- function killProcessTree(child) {
931
- if (process.platform === "win32" && child.pid) {
932
- try {
933
- spawn2("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
934
- windowsHide: true,
935
- stdio: "ignore"
936
- });
937
- } catch {
938
- 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;
939
1010
  }
940
- } else {
941
- child.kill("SIGTERM");
942
- setTimeout(() => {
943
- try {
944
- child.kill("SIGKILL");
945
- } catch {
946
- }
947
- }, 2e3);
948
- }
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();
949
1032
  }
950
1033
  async function executeBash(params) {
951
1034
  const trimmedCommand = params.command?.trim();
@@ -964,77 +1047,59 @@ async function executeBash(params) {
964
1047
  });
965
1048
  const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
966
1049
  const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
967
- const startTime = Date.now();
968
1050
  const shell = resolveShell();
969
- return new Promise((resolve3, reject) => {
970
- const stdoutChunks = [];
971
- let stdoutBytes = 0;
972
- const stderrChunks = [];
973
- let stderrBytes = 0;
974
- let truncated = false;
975
- let isKilled = false;
976
- const child = spawn2(trimmedCommand, {
977
- shell,
978
- cwd,
979
- env: { ...process.env, ...env },
980
- windowsHide: true
981
- });
982
- const killTimeoutTimer = setTimeout(() => {
983
- isKilled = true;
984
- killProcessTree(child);
985
- reject(new BashTimeoutError(normalizedTimeoutMs));
986
- }, normalizedTimeoutMs);
987
- child.stdout.on("data", (chunk) => {
988
- if (stdoutBytes < maxBuffer) {
989
- const remaining = maxBuffer - stdoutBytes;
990
- if (chunk.length <= remaining) {
991
- stdoutChunks.push(chunk);
992
- stdoutBytes += chunk.length;
993
- } else {
994
- stdoutChunks.push(chunk.subarray(0, remaining));
995
- stdoutBytes += remaining;
996
- truncated = true;
997
- }
998
- } else {
999
- truncated = true;
1000
- }
1001
- });
1002
- child.stderr.on("data", (chunk) => {
1003
- if (stderrBytes < maxBuffer) {
1004
- const remaining = maxBuffer - stderrBytes;
1005
- if (chunk.length <= remaining) {
1006
- stderrChunks.push(chunk);
1007
- stderrBytes += chunk.length;
1008
- } else {
1009
- stderrChunks.push(chunk.subarray(0, remaining));
1010
- stderrBytes += remaining;
1011
- truncated = true;
1012
- }
1013
- } else {
1014
- truncated = true;
1015
- }
1016
- });
1017
- child.on("error", (err) => {
1018
- clearTimeout(killTimeoutTimer);
1019
- reject(err);
1020
- });
1021
- child.on("close", (exitCode, signal) => {
1022
- clearTimeout(killTimeoutTimer);
1023
- if (isKilled) return;
1024
- const stdout = Buffer.concat(stdoutChunks).toString("utf8").trim();
1025
- const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
1026
- resolve3({
1027
- stdout,
1028
- stderr,
1029
- exitCode,
1030
- signal,
1031
- durationMs: Date.now() - startTime,
1032
- ...truncated ? { truncated: true } : {}
1033
- });
1034
- });
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
1035
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
+ };
1036
1101
  }
1037
- 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;
1038
1103
  var init_bash_executor = __esm({
1039
1104
  "src/bridge/bash-executor.ts"() {
1040
1105
  "use strict";
@@ -1052,11 +1117,51 @@ var init_bash_executor = __esm({
1052
1117
  MAX_TIMEOUT_MS = 6e5;
1053
1118
  DEFAULT_MAX_BUFFER = 512 * 1024;
1054
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
+ `;
1055
1160
  }
1056
1161
  });
1057
1162
 
1058
1163
  // src/bridge/directory-picker.ts
1059
- import { spawn as spawn3 } from "child_process";
1164
+ import { spawn as spawn2 } from "child_process";
1060
1165
  async function pickDirectory(optionsOrTitle) {
1061
1166
  const options = typeof optionsOrTitle === "string" ? { title: optionsOrTitle } : optionsOrTitle || {};
1062
1167
  const title = options.title || "\u8BF7\u9009\u62E9\u5DE5\u4F5C\u7A7A\u95F4\u6587\u4EF6\u5939";
@@ -1075,7 +1180,7 @@ function pickDirectoryMac(title, defaultPath) {
1075
1180
  const defaultClause = defaultPath ? ` default location POSIX file "${defaultPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : "";
1076
1181
  const appleScript = `POSIX path of (choose folder with prompt "${safeTitle}"${defaultClause})`;
1077
1182
  return new Promise((resolve3, reject) => {
1078
- const child = spawn3("osascript", ["-e", appleScript], {
1183
+ const child = spawn2("osascript", ["-e", appleScript], {
1079
1184
  stdio: ["ignore", "pipe", "pipe"]
1080
1185
  });
1081
1186
  let stdout = "";
@@ -1193,7 +1298,7 @@ $result = [FolderPicker]::Pick("${safeTitle}", "${safeDefaultPath}")
1193
1298
  if ($result) { Write-Output $result }
1194
1299
  `;
1195
1300
  return new Promise((resolve3, reject) => {
1196
- const child = spawn3("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1301
+ const child = spawn2("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1197
1302
  stdio: ["ignore", "pipe", "pipe"],
1198
1303
  windowsHide: true
1199
1304
  });
@@ -1224,7 +1329,7 @@ function pickDirectoryLinux(title, defaultPath) {
1224
1329
  args.push(`--filename=${defaultPath}`);
1225
1330
  }
1226
1331
  return new Promise((resolve3, reject) => {
1227
- const child = spawn3("zenity", args, {
1332
+ const child = spawn2("zenity", args, {
1228
1333
  stdio: ["ignore", "pipe", "pipe"]
1229
1334
  });
1230
1335
  let stdout = "";
@@ -1262,6 +1367,344 @@ var init_directory_picker = __esm({
1262
1367
  }
1263
1368
  });
1264
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
+
1265
1708
  // src/bridge/bridge-client.ts
1266
1709
  var bridge_client_exports = {};
1267
1710
  __export(bridge_client_exports, {
@@ -1276,7 +1719,7 @@ __export(bridge_client_exports, {
1276
1719
  });
1277
1720
  import { WebSocketServer, WebSocket as WsClient } from "ws";
1278
1721
  import { EventEmitter } from "events";
1279
- import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
1722
+ import { randomUUID as randomUUID2, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
1280
1723
  import fs4 from "fs";
1281
1724
  import path4 from "path";
1282
1725
  import os3 from "os";
@@ -1345,6 +1788,7 @@ var init_bridge_client = __esm({
1345
1788
  init_protocol();
1346
1789
  init_bash_executor();
1347
1790
  init_directory_picker();
1791
+ init_terminal_session();
1348
1792
  WS_PORT = DEFAULT_WS_PORT;
1349
1793
  REQUEST_TIMEOUT_MS = 3e4;
1350
1794
  HANDSHAKE_TIMEOUT_MS = 5e3;
@@ -1366,6 +1810,7 @@ var init_bridge_client = __esm({
1366
1810
  pending = /* @__PURE__ */ new Map();
1367
1811
  socketAuthStates = /* @__PURE__ */ new Map();
1368
1812
  destroyed = false;
1813
+ terminalSessions = null;
1369
1814
  idCounter = 0;
1370
1815
  isClientMode = false;
1371
1816
  explicitAuthToken;
@@ -1620,7 +2065,7 @@ var init_bridge_client = __esm({
1620
2065
  this.emit("error", err);
1621
2066
  });
1622
2067
  this.wss.on("connection", (socket) => {
1623
- const challenge = randomUUID();
2068
+ const challenge = randomUUID2();
1624
2069
  const hmacSalt = randomBytes(16).toString("hex");
1625
2070
  const authState = {
1626
2071
  authenticated: false,
@@ -1662,6 +2107,7 @@ var init_bridge_client = __esm({
1662
2107
  }
1663
2108
  if (socket === this.extensionSocket) {
1664
2109
  this.extensionSocket = null;
2110
+ this.terminalSessions?.disposeAll();
1665
2111
  this.emit("disconnected");
1666
2112
  for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1667
2113
  if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
@@ -1708,6 +2154,16 @@ var init_bridge_client = __esm({
1708
2154
  return;
1709
2155
  }
1710
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
+ }
1711
2167
  if (socket === this.extensionSocket) {
1712
2168
  await this.handleExtensionSystemRpc(socket, msg);
1713
2169
  return;
@@ -1871,9 +2327,6 @@ var init_bridge_client = __esm({
1871
2327
  systemRpcHandlers = Object.assign(
1872
2328
  /* @__PURE__ */ Object.create(null),
1873
2329
  {
1874
- [BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
1875
- return executeBash(params || {});
1876
- },
1877
2330
  [BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
1878
2331
  const selectedPath = await pickDirectory({
1879
2332
  title: params?.title || void 0,
@@ -1884,11 +2337,43 @@ var init_bridge_client = __esm({
1884
2337
  selectedPath,
1885
2338
  canceled: selectedPath === null
1886
2339
  };
1887
- }
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 || {})
1888
2345
  }
1889
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
+ }
1890
2371
  async handleExtensionSystemRpc(socket, msg) {
1891
2372
  const { id, method, params } = msg;
2373
+ if (method === BRIDGE_METHODS.SYSTEM_BASH) {
2374
+ await this.executeHostBash(socket, id, params);
2375
+ return;
2376
+ }
1892
2377
  const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
1893
2378
  if (!handler || typeof handler !== "function") {
1894
2379
  this.sendJsonRpcResponse(socket, {
@@ -1984,9 +2469,26 @@ var init_bridge_client = __esm({
1984
2469
  }
1985
2470
  });
1986
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
+ }
1987
2488
  /** 销毁服务,断开所有连接 */
1988
2489
  destroy() {
1989
2490
  this.destroyed = true;
2491
+ this.terminalSessions?.disposeAll();
1990
2492
  this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
1991
2493
  this.forwardedRequests.clear();
1992
2494
  if (this.clientSocket) {
@@ -2131,7 +2633,7 @@ __export(daemon_exports, {
2131
2633
  isDaemonProcessAlive: () => isDaemonProcessAlive,
2132
2634
  isProcessAlive: () => isProcessAlive,
2133
2635
  killProcessOnPort: () => killProcessOnPort,
2134
- killProcessTree: () => killProcessTree2,
2636
+ killProcessTree: () => killProcessTree,
2135
2637
  probeServerReady: () => probeServerReady,
2136
2638
  runDaemonProcess: () => runDaemonProcess,
2137
2639
  stopBridgeDaemon: () => stopBridgeDaemon
@@ -2190,7 +2692,7 @@ async function findPidByPort(port, excludePid) {
2190
2692
  return null;
2191
2693
  }
2192
2694
  }
2193
- async function killProcessTree2(pid) {
2695
+ async function killProcessTree(pid) {
2194
2696
  if (!pid || pid <= 0 || pid === process.pid) return false;
2195
2697
  if (process.platform === "win32") {
2196
2698
  try {
@@ -2228,7 +2730,7 @@ async function killProcessTree2(pid) {
2228
2730
  async function killProcessOnPort(port) {
2229
2731
  const pid = await findPidByPort(port, process.pid);
2230
2732
  if (pid) {
2231
- await killProcessTree2(pid);
2733
+ await killProcessTree(pid);
2232
2734
  for (let i = 0; i < 20; i++) {
2233
2735
  await new Promise((r) => setTimeout(r, 100));
2234
2736
  const remainingPid = await findPidByPort(port, process.pid);
@@ -2410,7 +2912,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2410
2912
  if (portPid && portPid !== daemonPid) {
2411
2913
  return false;
2412
2914
  }
2413
- const killed = await killProcessTree2(daemonPid);
2915
+ const killed = await killProcessTree(daemonPid);
2414
2916
  if (killed) {
2415
2917
  killedAny = true;
2416
2918
  try {
@@ -2421,7 +2923,7 @@ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2421
2923
  } else {
2422
2924
  if (daemonPid) {
2423
2925
  try {
2424
- const killed = await killProcessTree2(daemonPid);
2926
+ const killed = await killProcessTree(daemonPid);
2425
2927
  if (killed) {
2426
2928
  killedAny = true;
2427
2929
  try {
@@ -3544,11 +4046,20 @@ function handleCommandError(error, commandName, args) {
3544
4046
  }
3545
4047
  program.name("webmcp-cli").description(
3546
4048
  "WebMCP CLI \u2014 \u652F\u6301\u4E24\u79CD\u6D4F\u89C8\u5668\u8FDE\u63A5\u6A21\u5F0F\uFF1A\n --mode cdp\uFF08\u9ED8\u8BA4\uFF09: \u57FA\u4E8E Puppeteer + CDP \u76F4\u8FDE\u6D4F\u89C8\u5668\uFF08\u72EC\u7ACB profile\uFF09\n --mode wxt : \u57FA\u4E8E WebSocket + Chrome \u6269\u5C55\u6865\u63A5\uFF08\u590D\u7528\u7528\u6237\u6570\u636E\uFF09"
3547
- ).version(package_default.version).option("-w, --workspace <path>", "\u6307\u5B9A\u81EA\u5B9A\u4E49\u7684\u6D4F\u89C8\u5668\u5DE5\u4F5C\u7A7A\u95F4\uFF08\u7528\u6237\u914D\u7F6E\u76EE\u5F55\uFF09\u8DEF\u5F84\uFF0C\u4EC5 CDP \u6A21\u5F0F\u6709\u6548").hook("preAction", (thisCommand) => {
4049
+ ).version(package_default.version).option("-w, --workspace <path>", "\u6307\u5B9A\u81EA\u5B9A\u4E49\u7684\u6D4F\u89C8\u5668\u5DE5\u4F5C\u7A7A\u95F4\uFF08\u7528\u6237\u914D\u7F6E\u76EE\u5F55\uFF09\u8DEF\u5F84\uFF0C\u4EC5 CDP \u6A21\u5F0F\u6709\u6548").option(
4050
+ "--ignore-certificate-errors",
4051
+ "\u5173\u95ED\u53D7\u63A7\u6D4F\u89C8\u5668\u7684 HTTPS \u8BC1\u4E66\u6821\u9A8C\uFF08\u81EA\u7B7E\u540D\u3001\u8FC7\u671F\u6216\u4E3B\u673A\u540D\u4E0D\u5339\u914D\uFF09\u3002\u4E5F\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF WEBMCP_IGNORE_CERTIFICATE_ERRORS=1\u3002\u9ED8\u8BA4\u4ECD\u6821\u9A8C\u3002\u4EC5 CDP \u6A21\u5F0F\u6709\u6548"
4052
+ ).hook("preAction", (thisCommand) => {
3548
4053
  const opts = thisCommand.opts();
3549
4054
  if (opts.workspace) {
3550
4055
  process.env.WEBMCP_WORKSPACE = opts.workspace;
3551
4056
  }
4057
+ if (opts.ignoreCertificateErrors) {
4058
+ process.env.WEBMCP_IGNORE_CERTIFICATE_ERRORS = "1";
4059
+ }
4060
+ if (process.env.WEBMCP_IGNORE_CERTIFICATE_ERRORS === "1") {
4061
+ console.error(pc2.yellow("\u5F53\u524D\u53D7\u63A7\u6D4F\u89C8\u5668\u5DF2\u5173\u95ED TLS \u8BC1\u4E66\u6821\u9A8C\u3002"));
4062
+ }
3552
4063
  });
3553
4064
  program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D\u9875\u7B7E\u6216\u6307\u5B9A\u9875\u7B7E\u7684\u5BFC\u822A\u5143\u6570\u636E\uFF08url\u3001title\u3001webmcpTools\u3001\u6240\u6709\u9875\u7B7E\u5217\u8868\uFF09").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (options) => {
3554
4065
  const args = { tabid: parseTabId(options.tabid) };