@opentiny/webmcp-cli 0.0.6 → 0.0.9

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
@@ -1,192 +1,13 @@
1
1
  #!/usr/bin/env node
2
-
3
- // src/bin.ts
4
- import fs2 from "fs";
5
- import path2 from "path";
6
- import os3 from "os";
7
- import { Command } from "commander";
8
- import pc2 from "picocolors";
9
- import { readFileSync as readFileSync2 } from "fs";
10
- import { resolve as resolve2 } from "path";
11
-
12
- // src/expand-file-refs.ts
13
- import { readFileSync } from "fs";
14
- import { dirname, isAbsolute, normalize, resolve } from "path";
15
- import { fileURLToPath } from "url";
16
- var JSON_STRING_LITERAL = /"(?:[^"\\]|\\.)*"/g;
17
- var UNQUOTED_FILE_REF = /(?<![\\"])@(base64)?file:([^\s"',}\]]+)/g;
18
- var FILE_REF_PREFIX = /^@(base64)?file:(.+)$/s;
19
- function resolveFilePath(filePath, baseDir = process.cwd()) {
20
- let normalizedPath = filePath.trim();
21
- if (normalizedPath.startsWith('"') && normalizedPath.endsWith('"') || normalizedPath.startsWith("'") && normalizedPath.endsWith("'")) {
22
- normalizedPath = normalizedPath.slice(1, -1).trim();
23
- }
24
- if (normalizedPath.startsWith("file://")) {
25
- normalizedPath = fileURLToPath(normalizedPath);
26
- }
27
- if (isAbsolute(normalizedPath)) {
28
- return normalize(normalizedPath);
29
- }
30
- return resolve(baseDir, normalizedPath);
31
- }
32
- function decodeFilePath(rawPath) {
33
- return rawPath.replace(/\\\\/g, "\\");
34
- }
35
- function readFileRefContent(filePath, baseDir, base64) {
36
- const absPath = resolveFilePath(filePath, baseDir);
37
- let content;
38
- try {
39
- content = readFileSync(absPath, "utf-8");
40
- } catch (e) {
41
- throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}" (${absPath}): ${e.message}`);
42
- }
43
- return base64 ? Buffer.from(content, "utf-8").toString("base64") : content;
44
- }
45
- function expandFileRefValue(rawInner, baseDir) {
46
- const match = rawInner.match(FILE_REF_PREFIX);
47
- if (!match) {
48
- return null;
49
- }
50
- const [, base64Flag, rawPath] = match;
51
- const filePath = decodeFilePath(rawPath);
52
- const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
53
- return JSON.stringify(value);
54
- }
55
- function expandQuotedFileRefs(argsJson, baseDir) {
56
- return argsJson.replace(JSON_STRING_LITERAL, (jsonStringLiteral) => {
57
- const rawInner = jsonStringLiteral.slice(1, -1);
58
- const replacement = expandFileRefValue(rawInner, baseDir);
59
- return replacement ?? jsonStringLiteral;
60
- });
61
- }
62
- function expandUnquotedFileRefs(argsJson, baseDir) {
63
- return argsJson.replace(UNQUOTED_FILE_REF, (match, base64Flag, filePath) => {
64
- const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
65
- return JSON.stringify(value);
66
- });
67
- }
68
- function expandFileRefs(argsJson, baseDir = process.cwd()) {
69
- const withQuotedExpanded = expandQuotedFileRefs(argsJson, baseDir);
70
- return expandUnquotedFileRefs(withQuotedExpanded, baseDir);
71
- }
72
- function getFileBaseDir(fileOption) {
73
- if (!fileOption) {
74
- return process.cwd();
75
- }
76
- return dirname(resolve(process.cwd(), fileOption));
77
- }
78
-
79
- // src/parse-run-args.ts
80
- import JSON5 from "json5";
81
- function cleanJsonString(str) {
82
- let cleaned = str.trim();
83
- if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
84
- cleaned = cleaned.slice(1, -1).trim();
85
- }
86
- return cleaned;
87
- }
88
- function looksLikeObjectBody(raw) {
89
- return /^("?[\w-]+"?\s*:)/.test(raw);
90
- }
91
- function joinShellArgs(args) {
92
- const cleaned = args.map(cleanJsonString);
93
- const first = cleaned[0] ?? "";
94
- if (first.startsWith("{")) {
95
- return cleaned.join(" ");
96
- }
97
- if (looksLikeObjectBody(first)) {
98
- let joined = cleaned.join(" ");
99
- if (!joined.startsWith("{")) {
100
- joined = `{${joined}}`;
101
- }
102
- if (!joined.endsWith("}")) {
103
- joined = `${joined}}`;
104
- }
105
- return joined;
106
- }
107
- return cleaned.join(" ");
108
- }
109
- function insertMissingCommas(raw) {
110
- return raw.replace(/"(\s+)("?[\w-]+"?\s*:)/g, '",$2');
111
- }
112
- function quoteUnquotedStringValues(raw) {
113
- return raw.replace(
114
- /([{,]\s*(?:"([^"]+)"|([\w-]+))\s*:\s*)(?!"@(?:base64)?file:|"|true|false|null|-?\d|\{|\[)([^",}\]]+?)(?=\s*[,}])/g,
115
- (_match, prefix, _quotedKey, _unquotedKey, value) => {
116
- const trimmed = value.trim();
117
- if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed)) {
118
- return prefix + trimmed;
119
- }
120
- return prefix + JSON.stringify(trimmed);
121
- }
122
- );
123
- }
124
- function normalizeShellDamagedJson(raw) {
125
- let normalized = raw.trim();
126
- normalized = insertMissingCommas(normalized);
127
- normalized = quoteUnquotedStringValues(normalized);
128
- return normalized;
129
- }
130
- function buildArgsFromKeyValuePairs(args) {
131
- const obj = {};
132
- for (const pair of args) {
133
- const index = pair.indexOf("=");
134
- if (index === -1) {
135
- throw new Error(`\u65E0\u6548\u7684\u53C2\u6570\u683C\u5F0F "${pair}"\u3002\u8BF7\u4F7F\u7528 key=value\uFF0C\u6216 JSON / -f \u6587\u4EF6\u3002`);
136
- }
137
- const key = pair.substring(0, index).trim();
138
- let val = cleanJsonString(pair.substring(index + 1).trim());
139
- try {
140
- obj[key] = JSON.parse(val);
141
- } catch {
142
- obj[key] = val;
143
- }
144
- }
145
- return JSON.stringify(obj);
146
- }
147
- function parseArgsJson(raw) {
148
- const trimmed = cleanJsonString(raw);
149
- try {
150
- return JSON.stringify(JSON5.parse(trimmed));
151
- } catch (firstError) {
152
- const repaired = normalizeShellDamagedJson(trimmed);
153
- try {
154
- return JSON.stringify(JSON5.parse(repaired));
155
- } catch {
156
- const hint = `\u63D0\u793A\uFF1Abash \u4E2D JSON \u5FC5\u987B\u7528\u5355\u5F15\u53F7\u5305\u88F9\uFF0C\u4F8B\u5982 '{"title":"\u6807\u9898","content":"@base64file:./article.md"}'\uFF1B\u6216\u4F7F\u7528 key=value\uFF1Atitle=\u6807\u9898 content=@base64file:./article.md\uFF1B\u6216\u4F7F\u7528 -f args.json\u3002`;
157
- const message = firstError instanceof Error ? firstError.message : String(firstError);
158
- throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON \u6216 JS \u5BF9\u8C61: ${message}
159
- ${hint}`);
160
- }
161
- }
162
- }
163
- function prepareRunArgsJson(args, fileContent, baseDir) {
164
- if (fileContent !== void 0) {
165
- const trimmed = fileContent.trim();
166
- if (!trimmed) {
167
- throw new Error("\u901A\u8FC7 -f/--file \u6307\u5B9A\u7684\u53C2\u6570\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
168
- }
169
- const expanded2 = expandFileRefs(trimmed, baseDir);
170
- return parseArgsJson(expanded2);
171
- }
172
- if (args.length === 0) {
173
- return "{}";
174
- }
175
- const joined = joinShellArgs(args);
176
- let raw = "";
177
- if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
178
- raw = joined;
179
- } else if (args.every((arg) => arg.includes("="))) {
180
- raw = buildArgsFromKeyValuePairs(args);
181
- } else {
182
- raw = joined;
183
- }
184
- if (!raw.trim()) {
185
- throw new Error("\u672A\u63D0\u4F9B\u6709\u6548\u53C2\u6570");
186
- }
187
- const expanded = expandFileRefs(raw, baseDir);
188
- return parseArgsJson(expanded);
189
- }
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
190
11
 
191
12
  // src/browser.ts
192
13
  import puppeteer from "puppeteer-core";
@@ -197,13 +18,37 @@ import path from "path";
197
18
  import http from "http";
198
19
  import { spawn } from "child_process";
199
20
  import { fileURLToPath as fileURLToPath2 } from "url";
200
- var __filename = fileURLToPath2(import.meta.url);
201
- var __dirname = path.dirname(__filename);
202
- var CDP_PORT = 9222;
203
- var CDP_URL = `http://localhost:${CDP_PORT}`;
21
+ function getCdpPort() {
22
+ const raw = process.env.WEBMCP_CDP_PORT;
23
+ if (raw) {
24
+ const port = Number.parseInt(raw, 10);
25
+ if (Number.isFinite(port) && port > 0) return port;
26
+ }
27
+ return 9222;
28
+ }
204
29
  function getWorkspaceDir() {
205
30
  return process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
206
31
  }
32
+ function shouldLaunchHeadless() {
33
+ if (process.env.WEBMCP_HEADLESS === "0") return false;
34
+ return process.env.WEBMCP_HEADLESS === "1" || process.env.CI === "true" || process.env.CI === "1";
35
+ }
36
+ function readActivePortFromProfile(userDataDir) {
37
+ try {
38
+ const content = fs.readFileSync(path.join(userDataDir, "DevToolsActivePort"), "utf-8");
39
+ const port = parseInt(content.split("\n")[0].trim(), 10);
40
+ return Number.isFinite(port) && port > 0 ? port : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+ function candidateCdpBaseUrls(port) {
46
+ return [
47
+ `http://127.0.0.1:${port}`,
48
+ `http://[::1]:${port}`,
49
+ `http://localhost:${port}`
50
+ ];
51
+ }
207
52
  function getLastTabIdFilePath() {
208
53
  return path.join(getWorkspaceDir(), ".last-tab-id");
209
54
  }
@@ -297,6 +142,13 @@ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
297
142
  }
298
143
  }
299
144
  function getDefaultBrowserPath() {
145
+ const envPath = process.env.CHROME_PATH || process.env.WEBMCP_CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
146
+ if (envPath && fs.existsSync(envPath)) {
147
+ return { path: envPath, name: "Chrome" };
148
+ }
149
+ if (envPath) {
150
+ console.log(pc.yellow(`\u6307\u5B9A\u7684\u6D4F\u89C8\u5668\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5C06\u56DE\u9000\u5230\u81EA\u52A8\u68C0\u6D4B: ${envPath}`));
151
+ }
300
152
  const platform = os.platform();
301
153
  if (platform === "darwin") {
302
154
  const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
@@ -340,16 +192,30 @@ async function startBrowserInBackground() {
340
192
  if (!browserInfo || !fs.existsSync(browserInfo.path)) {
341
193
  throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6216 Edge \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
342
194
  }
343
- console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
344
- const userDataDir = process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
195
+ const cdpPort = getCdpPort();
196
+ console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${cdpPort})...`));
197
+ const userDataDir = getWorkspaceDir();
198
+ if (!fs.existsSync(userDataDir)) {
199
+ fs.mkdirSync(userDataDir, { recursive: true });
200
+ }
201
+ try {
202
+ fs.unlinkSync(path.join(userDataDir, "DevToolsActivePort"));
203
+ } catch {
204
+ }
345
205
  const launchArgs = [
346
- `--remote-debugging-port=${CDP_PORT}`,
206
+ `--remote-debugging-port=${cdpPort}`,
347
207
  `--user-data-dir=${userDataDir}`,
348
208
  "--no-first-run",
349
209
  "--no-default-browser-check"
350
210
  ];
351
- if (os.platform() === "win32") {
352
- launchArgs.push("--remote-debugging-address=127.0.0.1");
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
+ );
353
219
  }
354
220
  const child = spawn(
355
221
  browserInfo.path,
@@ -360,15 +226,22 @@ async function startBrowserInBackground() {
360
226
  }
361
227
  );
362
228
  child.unref();
363
- const pollUrls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
364
- for (let i = 0; i < 30; i++) {
365
- for (const url of pollUrls) {
366
- try {
367
- await httpGet(url, 1e3);
368
- console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
369
- await new Promise((resolve3) => setTimeout(resolve3, 500));
370
- return;
371
- } catch {
229
+ for (let i = 0; i < 40; i++) {
230
+ const ports = /* @__PURE__ */ new Set([cdpPort]);
231
+ const activePort = readActivePortFromProfile(userDataDir);
232
+ if (activePort) ports.add(activePort);
233
+ for (const port of ports) {
234
+ for (const baseUrl of candidateCdpBaseUrls(port)) {
235
+ try {
236
+ await httpGet(`${baseUrl}/json/version`, 1e3);
237
+ if (port !== cdpPort) {
238
+ console.log(pc.yellow(`\u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\uFF0C${browserInfo.name} \u5DF2\u81EA\u52A8\u5207\u6362\u5230\u7AEF\u53E3 ${port}\u3002`));
239
+ }
240
+ console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\uFF08${baseUrl}\uFF09\u3002`));
241
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
242
+ return baseUrl;
243
+ } catch {
244
+ }
372
245
  }
373
246
  }
374
247
  await new Promise((resolve3) => setTimeout(resolve3, 500));
@@ -376,13 +249,16 @@ async function startBrowserInBackground() {
376
249
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
377
250
  }
378
251
  async function connectBrowser() {
252
+ const userDataDir = getWorkspaceDir();
253
+ const cdpPort = getCdpPort();
254
+ const recordedPort = readActivePortFromProfile(userDataDir);
379
255
  const versionAddresses = [
380
- `http://127.0.0.1:${CDP_PORT}/json/version`,
381
- `http://localhost:${CDP_PORT}/json/version`
256
+ ...recordedPort && recordedPort !== cdpPort ? candidateCdpBaseUrls(recordedPort).map((u) => `${u}/json/version`) : [],
257
+ ...candidateCdpBaseUrls(cdpPort).map((u) => `${u}/json/version`)
382
258
  ];
383
259
  const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
384
260
  if (existingUrl) {
385
- console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
261
+ console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
386
262
  try {
387
263
  const browser = await connectPuppeteer(existingUrl);
388
264
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
@@ -394,25 +270,22 @@ async function connectBrowser() {
394
270
  );
395
271
  }
396
272
  }
397
- console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${CDP_PORT} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
273
+ console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${cdpPort} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
274
+ let launchedUrl;
398
275
  try {
399
- await startBrowserInBackground();
276
+ launchedUrl = await startBrowserInBackground();
400
277
  } catch (launchError) {
401
278
  const msg = launchError instanceof Error ? launchError.message : String(launchError);
402
279
  console.error(pc.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
403
280
  throw new Error("Browser launch failed.");
404
281
  }
405
- const launchedUrl = await findAvailableCdpUrl(versionAddresses, 5);
406
- if (!launchedUrl) {
407
- throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08\u7AEF\u53E3 ${CDP_PORT}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
408
- }
409
282
  try {
410
283
  console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
411
284
  const browser = await connectPuppeteer(launchedUrl);
412
285
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
413
286
  return browser;
414
287
  } catch {
415
- throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08\u7AEF\u53E3 ${CDP_PORT}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
288
+ throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08${launchedUrl}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
416
289
  }
417
290
  }
418
291
  async function getPageTargetId(page) {
@@ -492,10 +365,11 @@ async function getTargetPage(browser, tabid) {
492
365
  }
493
366
  if (!targetPage) {
494
367
  try {
495
- const urls = [
496
- `http://localhost:${CDP_PORT}/json/list`,
497
- `http://127.0.0.1:${CDP_PORT}/json/list`
498
- ];
368
+ const workspaceDir = getWorkspaceDir();
369
+ const cdpPort = getCdpPort();
370
+ const recordedPort = readActivePortFromProfile(workspaceDir);
371
+ const ports = recordedPort && recordedPort !== cdpPort ? [recordedPort, cdpPort] : [cdpPort];
372
+ const urls = ports.flatMap((port) => candidateCdpBaseUrls(port).map((u) => `${u}/json/list`));
499
373
  let activeTargetId = null;
500
374
  for (const url of urls) {
501
375
  try {
@@ -520,103 +394,2723 @@ async function getTargetPage(browser, tabid) {
520
394
  } catch {
521
395
  }
522
396
  }
523
- if (!targetPage) {
524
- const lastTarget = pageTargets[pageTargets.length - 1];
525
- targetPage = await lastTarget.page();
397
+ if (!targetPage) {
398
+ const lastTarget = pageTargets[pageTargets.length - 1];
399
+ targetPage = await lastTarget.page();
400
+ }
401
+ }
402
+ if (!targetPage) {
403
+ throw new Error("\u65E0\u6CD5\u83B7\u53D6\u76EE\u6807\u9875\u9762");
404
+ }
405
+ await injectWebMCPPolyfillAndTools(targetPage);
406
+ return targetPage;
407
+ }
408
+ async function injectIntoPage(page) {
409
+ await injectWebMCPPolyfillAndTools(page);
410
+ }
411
+ async function injectWebMCPPolyfillAndTools(page) {
412
+ const polyfillReady = await page.evaluate(() => {
413
+ return !!window.__webmcpcli_init;
414
+ }).catch(() => false);
415
+ if (!polyfillReady) {
416
+ console.log(pc.cyan("\u5F53\u524D\u9875\u9762\u5C1A\u672A\u6CE8\u5165 WebMCP \u73AF\u5883\uFF0C\u6B63\u5728\u6267\u884C\u81EA\u52A8\u6CE8\u5165..."));
417
+ const injectScriptPath = path.resolve(__dirname, "inject-bundle.js");
418
+ if (!fs.existsSync(injectScriptPath)) {
419
+ throw new Error(`Cannot find inject-bundle.js at ${injectScriptPath}. Please ensure you run 'pnpm build:inject' first.`);
420
+ }
421
+ const scriptContent = fs.readFileSync(injectScriptPath, "utf-8");
422
+ try {
423
+ await page.evaluate(scriptContent);
424
+ } catch (err) {
425
+ const msg = err instanceof Error ? err.message : String(err);
426
+ throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
427
+ }
428
+ await page.waitForFunction(() => {
429
+ const mcp = navigator.modelContext;
430
+ return mcp && typeof mcp.registerTool === "function";
431
+ }, { timeout: 1e4 }).catch(() => {
432
+ console.warn("\u7B49\u5F85 WebMCP registerTool \u63A5\u53E3\u5C31\u7EEA\u8D85\u65F6\uFF0C\u6B63\u5728\u7EE7\u7EED\u6CE8\u5165...");
433
+ });
434
+ }
435
+ await injectDomainTools(page);
436
+ }
437
+ function getToolsBundleName(hostname) {
438
+ const normalized = hostname.split(":")[0].toLowerCase();
439
+ const toolsDir = path.resolve(__dirname, "webmcp-tools");
440
+ if (!fs.existsSync(toolsDir)) {
441
+ return null;
442
+ }
443
+ let files = [];
444
+ try {
445
+ files = fs.readdirSync(toolsDir);
446
+ } catch {
447
+ return null;
448
+ }
449
+ const supportedDomains = files.filter((f) => f.endsWith(".js")).map((f) => f.slice(0, -3));
450
+ if (supportedDomains.includes(normalized)) {
451
+ return `webmcp-tools/${normalized}.js`;
452
+ }
453
+ for (const domain of supportedDomains) {
454
+ if (normalized.endsWith("." + domain)) {
455
+ return `webmcp-tools/${domain}.js`;
456
+ }
457
+ }
458
+ return null;
459
+ }
460
+ async function injectDomainTools(page) {
461
+ let hostname;
462
+ try {
463
+ const url = new URL(page.url());
464
+ hostname = url.hostname;
465
+ } catch {
466
+ return;
467
+ }
468
+ const bundleName = getToolsBundleName(hostname);
469
+ if (!bundleName) {
470
+ return;
471
+ }
472
+ const toolBundlePath = path.resolve(__dirname, bundleName);
473
+ if (!fs.existsSync(toolBundlePath)) {
474
+ return;
475
+ }
476
+ const bundleKey = bundleName.replace(/^webmcp-tools\//, "").replace(/\.js$/, "");
477
+ const flagKey = `__webmcptools_${bundleKey.replace(/[^a-zA-Z0-9]/g, "")}`;
478
+ const alreadyInjected = await page.evaluate((key) => !!window[key], flagKey).catch(() => false);
479
+ if (alreadyInjected) {
480
+ return;
481
+ }
482
+ console.log(pc.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
483
+ const toolScript = fs.readFileSync(toolBundlePath, "utf-8");
484
+ try {
485
+ await page.evaluate(toolScript);
486
+ console.log(pc.green(`\u5DF2\u4E3A ${hostname} \u6CE8\u5165\u9884\u7F6E\u5DE5\u5177`));
487
+ } catch (err) {
488
+ const msg = err instanceof Error ? err.message : String(err);
489
+ console.warn(pc.yellow(`\u57DF\u540D\u5DE5\u5177\u6CE8\u5165\u5931\u8D25 (${hostname}): ${msg}`));
490
+ }
491
+ }
492
+ var __filename, __dirname;
493
+ var init_browser = __esm({
494
+ "src/browser.ts"() {
495
+ "use strict";
496
+ __filename = fileURLToPath2(import.meta.url);
497
+ __dirname = path.dirname(__filename);
498
+ }
499
+ });
500
+
501
+ // package.json
502
+ var package_default;
503
+ var init_package = __esm({
504
+ "package.json"() {
505
+ package_default = {
506
+ name: "@opentiny/webmcp-cli",
507
+ version: "0.0.9",
508
+ type: "module",
509
+ description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
510
+ main: "dist/index.js",
511
+ types: "dist/index.d.ts",
512
+ bin: {
513
+ "webmcp-cli": "./dist/bin.js"
514
+ },
515
+ files: [
516
+ "dist",
517
+ "README.md"
518
+ ],
519
+ scripts: {
520
+ dev: "tsup --watch",
521
+ "build:inject": "node scripts/build-inject.mjs",
522
+ build: "tsup && pnpm run build:inject",
523
+ prepublishOnly: "pnpm run build",
524
+ "link:global": "npm install -g .",
525
+ start: "node dist/bin.js",
526
+ test: "vitest run",
527
+ "test:watch": "vitest",
528
+ "test:browser": "vitest run --config vitest.e2e.config.ts"
529
+ },
530
+ keywords: [
531
+ "chrome",
532
+ "puppeteer",
533
+ "cli"
534
+ ],
535
+ license: "MIT",
536
+ author: "OpenTiny Team",
537
+ engines: {
538
+ node: ">=16"
539
+ },
540
+ publishConfig: {
541
+ access: "public"
542
+ },
543
+ dependencies: {
544
+ "@mcp-b/webmcp-polyfill": "catalog:",
545
+ "@modelcontextprotocol/sdk": "catalog:",
546
+ "@opentiny/next-sdk": "workspace:^",
547
+ "@page-agent/page-controller": "catalog:",
548
+ clipboardy: "^5.3.1",
549
+ commander: "^11.1.0",
550
+ json5: "^2.2.3",
551
+ marked: "^15.0.7",
552
+ picocolors: "^1.0.0",
553
+ "puppeteer-core": "^22.0.0",
554
+ ws: "^8.18.0",
555
+ zod: "^3.23.0"
556
+ },
557
+ devDependencies: {
558
+ "@types/node": "^20.0.0",
559
+ "@types/ws": "^8.5.10",
560
+ esbuild: "^0.25.0",
561
+ tsup: "^8.0.2",
562
+ tsx: "^4.0.0",
563
+ typescript: "^5.3.3",
564
+ vitest: "^4.1.10"
565
+ }
566
+ };
567
+ }
568
+ });
569
+
570
+ // src/mcp/tools.ts
571
+ import { z } from "zod";
572
+ var BrowserStateInput, BrowserToolCallInput, BrowserTabsOpenInput, BrowserTabsCloseInput, BrowserTabsSwitchInput, BrowserTabsNavInput, BrowserSubAgentRunInput, BrowserSkillsListInput, BrowserSkillsReadInput;
573
+ var init_tools = __esm({
574
+ "src/mcp/tools.ts"() {
575
+ "use strict";
576
+ BrowserStateInput = z.object({
577
+ tabId: z.union([z.string(), z.number()]).optional().describe(
578
+ "\u76EE\u6807 tab ID\u3002CDP \u6A21\u5F0F\u4E3A target UUID \u5B57\u7B26\u4E32\uFF1BWXT \u6A21\u5F0F\u4E3A Chrome tab ID \u6570\u503C\u3002\u4E0D\u4F20\u65F6\u4F7F\u7528\u5F53\u524D\u6D3B\u8DC3 tab\u3002"
579
+ )
580
+ });
581
+ BrowserToolCallInput = z.object({
582
+ toolName: z.string().describe("\u9875\u9762 WebMCP \u5DE5\u5177\u540D\u79F0\uFF08\u53EF\u4ECE browser_state \u8FD4\u56DE\u7684 webmcpTools \u5217\u8868\u4E2D\u83B7\u53D6\uFF09"),
583
+ toolArgs: z.record(z.unknown()).optional().describe("\u5DE5\u5177\u53C2\u6570\uFF0C\u4E0E\u5DE5\u5177\u7684 inputSchema \u5BF9\u9F50"),
584
+ tabId: z.union([z.string(), z.number()]).optional().describe("\u76EE\u6807 tab ID\uFF0C\u4E0D\u4F20\u65F6\u4F7F\u7528\u5F53\u524D\u6D3B\u8DC3 tab")
585
+ });
586
+ BrowserTabsOpenInput = z.object({
587
+ url: z.string().url().refine((val) => /^https?:\/\//i.test(val), { message: "\u53EA\u5141\u8BB8\u6253\u5F00 http \u6216 https \u534F\u8BAE\u7684 URL" }).describe("\u8981\u6253\u5F00\u7684 URL\uFF08\u4EC5\u652F\u6301 http/https \u534F\u8BAE\uFF09")
588
+ });
589
+ BrowserTabsCloseInput = z.object({
590
+ tabId: z.union([z.string(), z.number()]).optional().describe(
591
+ "\u8981\u5173\u95ED\u7684 tab ID\uFF0C\u4E0D\u4F20\u65F6\u5173\u95ED\u5F53\u524D\u6D3B\u8DC3 tab"
592
+ )
593
+ });
594
+ BrowserTabsSwitchInput = z.object({
595
+ tabId: z.union([z.string(), z.number()]).describe(
596
+ "\u8981\u5207\u6362\u5230\u7684 tab ID\uFF08\u53EF\u4ECE browser_state \u8FD4\u56DE\u7684 tabs \u5217\u8868\u4E2D\u83B7\u53D6\uFF09"
597
+ ),
598
+ activateVisible: z.boolean().optional().describe("\u662F\u5426\u5728\u524D\u53F0\u663E\u5F0F\u5207\u6362 Chrome \u53EF\u89C1\u6D3B\u52A8\u6807\u7B7E\uFF08\u4EC5 WXT \u6A21\u5F0F\u6709\u6548\uFF0C\u9ED8\u8BA4 false\uFF09")
599
+ });
600
+ BrowserTabsNavInput = z.object({
601
+ tabId: z.union([z.string(), z.number()]).optional().describe("\u76EE\u6807 tab ID\uFF0C\u4E0D\u4F20\u65F6\u4F7F\u7528\u5F53\u524D\u6D3B\u8DC3 tab")
602
+ });
603
+ BrowserSubAgentRunInput = z.object({
604
+ instruction: z.string().describe("\u8981\u59D4\u6258\u7ED9 Tiny Robot \u5B50\u4EE3\u7406\u6267\u884C\u7684\u9AD8\u5C42\u81EA\u7136\u8BED\u8A00\u4EFB\u52A1\u6307\u4EE4"),
605
+ tabId: z.number().optional().describe("\u76EE\u6807 tab ID\uFF0C\u4E0D\u4F20\u65F6\u4F7F\u7528\u5F53\u524D\u6D3B\u8DC3 tab"),
606
+ maxSteps: z.number().min(1).max(20).optional().describe("\u6700\u5927\u81EA\u4E3B\u89C4\u5212\u6267\u884C\u6B65\u6570\uFF08\u9ED8\u8BA4 8\uFF0C\u6700\u5927 20\uFF09")
607
+ });
608
+ BrowserSkillsListInput = z.object({}).describe("\u83B7\u53D6\u5F53\u524D\u6D4F\u89C8\u5668\u6269\u5C55\u4E2D\u5DF2\u6CE8\u518C\u7684\u6240\u6709 Skills \u6280\u80FD\u5143\u6570\u636E\u5217\u8868");
609
+ BrowserSkillsReadInput = z.object({
610
+ name: z.string().describe("\u8981\u8BFB\u53D6\u7684\u6280\u80FD\u540D\u79F0\uFF08\u53EF\u4ECE browser_skills_list \u8FD4\u56DE\u7684\u5217\u8868\u4E2D\u83B7\u53D6\uFF09")
611
+ });
612
+ }
613
+ });
614
+
615
+ // src/adapters/cdp-adapter.ts
616
+ var cdp_adapter_exports = {};
617
+ __export(cdp_adapter_exports, {
618
+ CdpBrowserAdapter: () => CdpBrowserAdapter
619
+ });
620
+ var CdpBrowserAdapter;
621
+ var init_cdp_adapter = __esm({
622
+ "src/adapters/cdp-adapter.ts"() {
623
+ "use strict";
624
+ init_browser();
625
+ CdpBrowserAdapter = class {
626
+ browser = null;
627
+ async getBrowser() {
628
+ if (!this.browser) {
629
+ this.browser = await connectBrowser();
630
+ }
631
+ return this.browser;
632
+ }
633
+ async getState(tabId) {
634
+ const browser = await this.getBrowser();
635
+ const tabIdStr = tabId != null ? String(tabId) : void 0;
636
+ const page = await getTargetPage(browser, tabIdStr);
637
+ await injectIntoPage(page);
638
+ const state = await page.evaluate(async () => {
639
+ const url = document.URL;
640
+ const title = document.title;
641
+ const mcp = document.modelContext || navigator.modelContext;
642
+ let webmcpTools = [];
643
+ if (mcp && typeof mcp.getTools === "function") {
644
+ try {
645
+ const toolsResult = await mcp.getTools();
646
+ const list = toolsResult?.tools || toolsResult || [];
647
+ webmcpTools = list.filter((t) => t && typeof t.name === "string").map((t) => ({
648
+ name: t.name,
649
+ description: typeof t.description === "string" ? t.description.slice(0, 200) : void 0
650
+ }));
651
+ } catch {
652
+ webmcpTools = [];
653
+ }
654
+ }
655
+ return { url, title, webmcpTools };
656
+ });
657
+ const activeTabId = await getPageTargetId(page).catch(() => null);
658
+ const pages = await browser.pages();
659
+ const tabs2 = await Promise.all(
660
+ pages.map(async (p) => {
661
+ const pUrl = p.url();
662
+ if (pUrl.startsWith("devtools://")) return null;
663
+ const pTitle = await Promise.race([
664
+ p.title().catch(() => "Unknown"),
665
+ new Promise((resolve3) => setTimeout(() => resolve3("Unknown"), 500))
666
+ ]);
667
+ return {
668
+ tabId: await getPageTargetId(p).catch(() => pUrl),
669
+ title: pTitle,
670
+ url: pUrl
671
+ };
672
+ })
673
+ );
674
+ return {
675
+ ...state,
676
+ activeTabId: activeTabId ?? void 0,
677
+ tabs: tabs2.filter(Boolean)
678
+ };
679
+ }
680
+ async callTool(toolName, args, tabId) {
681
+ const browser = await this.getBrowser();
682
+ const tabIdStr = tabId != null ? String(tabId) : void 0;
683
+ const page = await getTargetPage(browser, tabIdStr);
684
+ const urlBefore = page.url();
685
+ await injectIntoPage(page);
686
+ const argsJson = JSON.stringify(args);
687
+ try {
688
+ const result = await page.evaluate(
689
+ async (name, inputString) => {
690
+ const mcp = document.modelContext || navigator.modelContext;
691
+ if (!mcp || typeof mcp.executeTool !== "function") {
692
+ throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (document.modelContext \u672A\u627E\u5230)");
693
+ }
694
+ const tools = await mcp.getTools();
695
+ const toolObj = tools.find((t) => t.name === name);
696
+ if (!toolObj) throw new Error(`Tool ${name} not found`);
697
+ let res = await mcp.executeTool(toolObj, inputString);
698
+ if (typeof res === "string") {
699
+ try {
700
+ res = JSON.parse(res);
701
+ } catch {
702
+ }
703
+ }
704
+ if (res === void 0 || res === null) {
705
+ throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C");
706
+ }
707
+ if (typeof res === "object" && res.success === false) {
708
+ const failed = res;
709
+ throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
710
+ }
711
+ return res;
712
+ },
713
+ toolName,
714
+ argsJson
715
+ );
716
+ return result;
717
+ } catch (evalError) {
718
+ const errMsg = evalError instanceof Error ? evalError.message : String(evalError);
719
+ const isContextDestroyed = errMsg.includes("context was destroyed") || errMsg.includes("Execution context was destroyed") || errMsg.includes("Cannot read properties of null") && errMsg.includes("context");
720
+ if (isContextDestroyed) {
721
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
722
+ const newUrl = page.url();
723
+ if (newUrl && newUrl !== urlBefore) {
724
+ return { success: true, message: `\u5DE5\u5177 ${toolName} \u6267\u884C\u5B8C\u6210\uFF0C\u9875\u9762\u5DF2\u5BFC\u822A\u81F3 ${newUrl}`, navigatedTo: newUrl };
725
+ }
726
+ }
727
+ throw evalError;
728
+ }
729
+ }
730
+ async tabs(action, options) {
731
+ const browser = await this.getBrowser();
732
+ const tabIdStr = options.tabId != null ? String(options.tabId) : void 0;
733
+ switch (action) {
734
+ case "open": {
735
+ if (!options.url) throw new Error("tabs open \u9700\u8981 url \u53C2\u6570");
736
+ const page = await browser.newPage();
737
+ await page.goto(options.url);
738
+ await injectIntoPage(page);
739
+ const tabId = await getPageTargetId(page);
740
+ setLastActiveTabId(tabId);
741
+ return { success: true, tabId, url: options.url };
742
+ }
743
+ case "close": {
744
+ const page = await getTargetPage(browser, tabIdStr);
745
+ await page.close();
746
+ return { success: true };
747
+ }
748
+ case "switch": {
749
+ if (!tabIdStr) throw new Error("tabs switch \u9700\u8981 tabId \u53C2\u6570");
750
+ await activateTabById(browser, tabIdStr);
751
+ setLastActiveTabId(tabIdStr);
752
+ return { success: true, tabId: tabIdStr };
753
+ }
754
+ case "back": {
755
+ const page = await getTargetPage(browser, tabIdStr);
756
+ await page.goBack();
757
+ return { success: true };
758
+ }
759
+ case "forward": {
760
+ const page = await getTargetPage(browser, tabIdStr);
761
+ await page.goForward();
762
+ return { success: true };
763
+ }
764
+ }
765
+ }
766
+ async dispose() {
767
+ if (this.browser) {
768
+ await this.browser.disconnect().catch(() => {
769
+ });
770
+ this.browser = null;
771
+ }
772
+ }
773
+ };
774
+ }
775
+ });
776
+
777
+ // src/bridge/protocol.ts
778
+ var DEFAULT_WS_PORT, PROTOCOL_VERSION, WS_CLOSE_CODE, BRIDGE_ERROR_CODE, BRIDGE_METHODS;
779
+ var init_protocol = __esm({
780
+ "src/bridge/protocol.ts"() {
781
+ "use strict";
782
+ DEFAULT_WS_PORT = 18999;
783
+ PROTOCOL_VERSION = "1.1.0";
784
+ WS_CLOSE_CODE = {
785
+ /** 被新接入的扩展连接平滑替换(旧连接触发退避重连) */
786
+ REPLACED: 4e3,
787
+ /** 认证未通过(未提供 Token、Token 错误或 HMAC 签名校验失败) */
788
+ UNAUTHORIZED: 4001,
789
+ /** 协议主版本号不匹配 */
790
+ PROTOCOL_MISMATCH: 4002
791
+ };
792
+ BRIDGE_ERROR_CODE = {
793
+ /** 请求执行超时 */
794
+ REQUEST_TIMEOUT: -32e3,
795
+ /** 命令执行超时 */
796
+ EXECUTION_TIMEOUT: -32001,
797
+ /** 命令或系统操作执行失败 */
798
+ EXECUTION_FAILED: -32002,
799
+ /** 无效参数(如 command 为空) */
800
+ INVALID_PARAMS: -32602,
801
+ /** 不支持的方法 */
802
+ METHOD_NOT_FOUND: -32601,
803
+ /** 扩展尚未连接至桥接服务 */
804
+ EXTENSION_NOT_CONNECTED: -32051,
805
+ /** 侧边栏尚未就绪或已关闭 */
806
+ SIDEPANEL_NOT_READY: -32050
807
+ };
808
+ BRIDGE_METHODS = {
809
+ TOOL_CALL: "tool/call",
810
+ SUB_AGENT_RUN: "sub_agent/run",
811
+ BROWSER_TABS: "browser/tabs",
812
+ BROWSER_STATE: "browser/state",
813
+ SKILLS_LIST: "skills/list",
814
+ SKILLS_GET: "skills/get",
815
+ SKILLS_READ_RESOURCE: "skills/read_resource",
816
+ SYSTEM_BASH: "system/bash",
817
+ SYSTEM_PICK_DIRECTORY: "system/pick_directory"
818
+ };
819
+ }
820
+ });
821
+
822
+ // src/bridge/workspace-manager.ts
823
+ import path2 from "path";
824
+ import os2 from "os";
825
+ import fs2 from "fs";
826
+ function resolveBaseWorkspace(params) {
827
+ if (params.workspacePath && typeof params.workspacePath === "string" && params.workspacePath.trim()) {
828
+ const trimmedWs = params.workspacePath.trim();
829
+ return path2.isAbsolute(trimmedWs) ? trimmedWs : path2.resolve(os2.homedir(), trimmedWs);
830
+ }
831
+ if (params.conversationId && typeof params.conversationId === "string" && params.conversationId.trim()) {
832
+ const safeId = params.conversationId.trim().replace(/[/\\?%*:|"<>.]/g, "_");
833
+ const sessionDir = path2.resolve(WORKSPACES_ROOT, safeId || "default");
834
+ const relative = path2.relative(WORKSPACES_ROOT, sessionDir);
835
+ if (!relative || relative.startsWith("..") || path2.isAbsolute(relative)) {
836
+ return path2.join(WORKSPACES_ROOT, "default");
837
+ }
838
+ return sessionDir;
839
+ }
840
+ return path2.join(WORKSPACES_ROOT, "default");
841
+ }
842
+ function resolveWorkspaceDir(params = {}) {
843
+ const baseDir = resolveBaseWorkspace(params);
844
+ if (!fs2.existsSync(baseDir)) {
845
+ try {
846
+ fs2.mkdirSync(baseDir, { recursive: true, mode: 493 });
847
+ } catch {
848
+ }
849
+ }
850
+ if (!params.cwd || typeof params.cwd !== "string" || !params.cwd.trim()) {
851
+ return baseDir;
852
+ }
853
+ const trimmedCwd = params.cwd.trim();
854
+ const targetDir = path2.isAbsolute(trimmedCwd) ? path2.resolve(trimmedCwd) : path2.resolve(baseDir, trimmedCwd);
855
+ const relative = path2.relative(baseDir, targetDir);
856
+ if (relative.startsWith("..") || path2.isAbsolute(relative)) {
857
+ throw new BashInvalidParamsError(`\u76EE\u6807\u6267\u884C\u8DEF\u5F84\u8D85\u51FA\u5DF2\u6388\u6743\u5DE5\u4F5C\u7A7A\u95F4\u5B89\u5168\u8FB9\u754C: ${trimmedCwd}`);
858
+ }
859
+ return targetDir;
860
+ }
861
+ var WORKSPACES_ROOT, BashInvalidParamsError;
862
+ var init_workspace_manager = __esm({
863
+ "src/bridge/workspace-manager.ts"() {
864
+ "use strict";
865
+ WORKSPACES_ROOT = path2.join(os2.homedir(), ".robot-wxt", "workspaces");
866
+ BashInvalidParamsError = class extends Error {
867
+ code = "INVALID_PARAMS";
868
+ constructor(message = "\u53C2\u6570\u9519\u8BEF") {
869
+ super(message);
870
+ this.name = "BashInvalidParamsError";
871
+ }
872
+ };
873
+ }
874
+ });
875
+
876
+ // src/bridge/bash-executor.ts
877
+ import fs3 from "fs";
878
+ import path3 from "path";
879
+ import { spawn as spawn2 } from "child_process";
880
+ function findWindowsBash() {
881
+ const pathDirs = (process.env.PATH || "").split(path3.delimiter);
882
+ for (const dir of pathDirs) {
883
+ if (!dir) continue;
884
+ const candidate = path3.join(dir, "bash.exe");
885
+ try {
886
+ if (fs3.existsSync(candidate)) return candidate;
887
+ } catch {
888
+ }
889
+ }
890
+ const localAppData = process.env.LOCALAPPDATA || "";
891
+ const programFiles = process.env.ProgramFiles || "C:\\Program Files";
892
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
893
+ const programW6432 = process.env.ProgramW6432 || programFiles;
894
+ const commonGitBashPaths = [
895
+ path3.join(programFiles, "Git", "bin", "bash.exe"),
896
+ path3.join(programFiles, "Git", "usr", "bin", "bash.exe"),
897
+ path3.join(programW6432, "Git", "bin", "bash.exe"),
898
+ path3.join(programFilesX86, "Git", "bin", "bash.exe"),
899
+ path3.join(localAppData, "Programs", "Git", "bin", "bash.exe"),
900
+ path3.join(localAppData, "Programs", "Git", "usr", "bin", "bash.exe"),
901
+ path3.join(process.env.SystemRoot || "C:\\Windows", "System32", "bash.exe")
902
+ ];
903
+ for (const candidate of commonGitBashPaths) {
904
+ try {
905
+ if (fs3.existsSync(candidate)) return candidate;
906
+ } catch {
907
+ }
908
+ }
909
+ return null;
910
+ }
911
+ function resolveShell(forcePlatform) {
912
+ const currentPlatform = forcePlatform || process.platform;
913
+ if (!forcePlatform && cachedShell) {
914
+ return cachedShell;
915
+ }
916
+ if (currentPlatform !== "win32") {
917
+ const posixShell = process.env.SHELL || "/bin/bash";
918
+ if (!forcePlatform) cachedShell = posixShell;
919
+ return posixShell;
920
+ }
921
+ const winBash = findWindowsBash();
922
+ if (winBash) {
923
+ if (!forcePlatform) cachedShell = winBash;
924
+ return winBash;
925
+ }
926
+ const fallback = "powershell.exe";
927
+ if (!forcePlatform) cachedShell = fallback;
928
+ return fallback;
929
+ }
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");
939
+ }
940
+ } else {
941
+ child.kill("SIGTERM");
942
+ setTimeout(() => {
943
+ try {
944
+ child.kill("SIGKILL");
945
+ } catch {
946
+ }
947
+ }, 2e3);
948
+ }
949
+ }
950
+ async function executeBash(params) {
951
+ const trimmedCommand = params.command?.trim();
952
+ if (!trimmedCommand) {
953
+ throw new BashInvalidParamsError("\u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A");
954
+ }
955
+ if (/rm\s+(-[a-zA-Z]*r[a-zA-Z]*f[a-zA-Z]*\s+|--recursive\s+--force\s+)\s*(\/|~|\$HOME|\/\*)\s*$/i.test(
956
+ trimmedCommand
957
+ ) || /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/i.test(trimmedCommand)) {
958
+ throw new BashInvalidParamsError("\u5B89\u5168\u62E6\u622A\uFF1A\u7981\u6B62\u5728\u5BBF\u4E3B\u673A\u6267\u884C\u9488\u5BF9\u7CFB\u7EDF\u5173\u952E\u6839\u8DEF\u5F84\u7684\u9AD8\u5371\u7834\u574F\u6027\u547D\u4EE4");
959
+ }
960
+ const cwd = resolveWorkspaceDir({
961
+ cwd: params.cwd,
962
+ workspacePath: params.workspacePath,
963
+ conversationId: params.conversationId
964
+ });
965
+ const normalizedTimeoutMs = typeof params.timeoutMs === "number" && Number.isFinite(params.timeoutMs) && params.timeoutMs > 0 ? Math.min(params.timeoutMs, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
966
+ const { env = {}, maxBuffer = DEFAULT_MAX_BUFFER } = params;
967
+ const startTime = Date.now();
968
+ 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
+ });
1035
+ });
1036
+ }
1037
+ var BashTimeoutError, DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS, DEFAULT_MAX_BUFFER, cachedShell;
1038
+ var init_bash_executor = __esm({
1039
+ "src/bridge/bash-executor.ts"() {
1040
+ "use strict";
1041
+ init_workspace_manager();
1042
+ BashTimeoutError = class extends Error {
1043
+ code = "TIMEOUT";
1044
+ timeoutMs;
1045
+ constructor(timeoutMs) {
1046
+ super(`\u547D\u4EE4\u6267\u884C\u8D85\u65F6 (${timeoutMs / 1e3}s)`);
1047
+ this.name = "BashTimeoutError";
1048
+ this.timeoutMs = timeoutMs;
1049
+ }
1050
+ };
1051
+ DEFAULT_TIMEOUT_MS = 3e4;
1052
+ MAX_TIMEOUT_MS = 6e5;
1053
+ DEFAULT_MAX_BUFFER = 512 * 1024;
1054
+ cachedShell = null;
1055
+ }
1056
+ });
1057
+
1058
+ // src/bridge/directory-picker.ts
1059
+ import { spawn as spawn3 } from "child_process";
1060
+ async function pickDirectory(optionsOrTitle) {
1061
+ const options = typeof optionsOrTitle === "string" ? { title: optionsOrTitle } : optionsOrTitle || {};
1062
+ const title = options.title || "\u8BF7\u9009\u62E9\u5DE5\u4F5C\u7A7A\u95F4\u6587\u4EF6\u5939";
1063
+ const defaultPath = options.defaultPath && typeof options.defaultPath === "string" ? options.defaultPath.trim() : void 0;
1064
+ const platform = process.platform;
1065
+ if (platform === "darwin") {
1066
+ return pickDirectoryMac(title, defaultPath);
1067
+ }
1068
+ if (platform === "win32") {
1069
+ return pickDirectoryWindows(title, defaultPath);
1070
+ }
1071
+ return pickDirectoryLinux(title, defaultPath);
1072
+ }
1073
+ function pickDirectoryMac(title, defaultPath) {
1074
+ const safeTitle = title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1075
+ const defaultClause = defaultPath ? ` default location POSIX file "${defaultPath.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : "";
1076
+ const appleScript = `POSIX path of (choose folder with prompt "${safeTitle}"${defaultClause})`;
1077
+ return new Promise((resolve3, reject) => {
1078
+ const child = spawn3("osascript", ["-e", appleScript], {
1079
+ stdio: ["ignore", "pipe", "pipe"]
1080
+ });
1081
+ let stdout = "";
1082
+ let stderr = "";
1083
+ child.stdout?.on("data", (d) => {
1084
+ stdout += d.toString();
1085
+ });
1086
+ child.stderr?.on("data", (d) => {
1087
+ stderr += d.toString();
1088
+ });
1089
+ child.on("close", (code) => {
1090
+ if (code === 0) {
1091
+ const selected = stdout.trim();
1092
+ resolve3(selected || null);
1093
+ return;
1094
+ }
1095
+ if (code === 1 && (stderr.includes("User canceled") || stderr.includes("-128") || !stderr.trim())) {
1096
+ resolve3(null);
1097
+ return;
1098
+ }
1099
+ reject(new Error(`\u5524\u8D77\u7CFB\u7EDF\u6587\u4EF6\u5939\u9009\u62E9\u5668\u5931\u8D25: ${stderr.trim() || `\u9000\u51FA\u7801 ${code}`}`));
1100
+ });
1101
+ child.on("error", (err) => {
1102
+ reject(new Error(`\u5524\u8D77\u7CFB\u7EDF\u6587\u4EF6\u5939\u9009\u62E9\u5668\u5931\u8D25: ${err.message}`));
1103
+ });
1104
+ });
1105
+ }
1106
+ function escapePowerShellString(str) {
1107
+ return str.replace(/`/g, "``").replace(/"/g, '`"').replace(/\$/g, "`$");
1108
+ }
1109
+ function pickDirectoryWindows(title, defaultPath) {
1110
+ const safeTitle = escapePowerShellString(title);
1111
+ const safeDefaultPath = escapePowerShellString(defaultPath ?? "");
1112
+ const psScript = `
1113
+ $code = @'
1114
+ using System;
1115
+ using System.Runtime.InteropServices;
1116
+
1117
+ [ComImport, Guid("D57C7288-D4AD-4768-BE02-9D969532D960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
1118
+ interface IFileOpenDialog {
1119
+ [PreserveSig] int Show(IntPtr hwnd);
1120
+ void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec);
1121
+ void SetFileTypeIndex(uint iFileType);
1122
+ void GetFileTypeIndex(out uint piFileType);
1123
+ void Advise(IntPtr pfde, out uint pdwCookie);
1124
+ void Unadvise(uint dwCookie);
1125
+ void SetOptions(uint fos);
1126
+ void GetOptions(out uint pfos);
1127
+ void SetDefaultFolder(IntPtr psi);
1128
+ void SetFolder(IntPtr psi);
1129
+ void GetFolder(out IntPtr ppsi);
1130
+ void GetCurrentSelection(out IntPtr ppsi);
1131
+ void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
1132
+ void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
1133
+ void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
1134
+ void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
1135
+ void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
1136
+ void GetResult(out IntPtr ppsi);
1137
+ void AddPlace(IntPtr psi, int fdap);
1138
+ void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
1139
+ void Close(int hr);
1140
+ void SetClientGuid([In] ref Guid guid);
1141
+ void ClearClientData();
1142
+ void SetFilter(IntPtr pFilter);
1143
+ void GetResults(out IntPtr ppenum);
1144
+ void GetSelectedItems(out IntPtr ppsai);
1145
+ }
1146
+
1147
+ [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
1148
+ interface IShellItem {
1149
+ void BindToHandler(IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, out IntPtr ppv);
1150
+ void GetParent(out IShellItem ppsi);
1151
+ void GetDisplayName(uint sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
1152
+ void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs);
1153
+ void Compare(IShellItem psi, uint hint, out int piOrder);
1154
+ }
1155
+
1156
+ public static class FolderPicker {
1157
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
1158
+ static extern int SHCreateItemFromParsingName(string pszPath, IntPtr pbc, [In] ref Guid riid, out IntPtr ppv);
1159
+
1160
+ // FOS_PICKFOLDERS = 0x20, FOS_FORCEFILESYSTEM = 0x40
1161
+ const uint FOS_PICKFOLDERS = 0x00000020;
1162
+ const uint FOS_FORCEFILESYSTEM = 0x00000040;
1163
+
1164
+ public static string Pick(string title, string defaultPath) {
1165
+ var clsid = new Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7");
1166
+ var riid = new Guid("D57C7288-D4AD-4768-BE02-9D969532D960");
1167
+ var obj = Activator.CreateInstance(Type.GetTypeFromCLSID(clsid));
1168
+ var dialog = (IFileOpenDialog)obj;
1169
+ dialog.SetOptions(FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM);
1170
+ dialog.SetTitle(title);
1171
+ if (!string.IsNullOrEmpty(defaultPath)) {
1172
+ var riidShellItem = new Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE");
1173
+ IntPtr psi;
1174
+ if (SHCreateItemFromParsingName(defaultPath, IntPtr.Zero, ref riidShellItem, out psi) == 0) {
1175
+ dialog.SetFolder(psi);
1176
+ Marshal.Release(psi);
1177
+ }
1178
+ }
1179
+ int hr = dialog.Show(IntPtr.Zero);
1180
+ if (hr != 0) return null; // \u7528\u6237\u53D6\u6D88 (HRESULT_FROM_WIN32(ERROR_CANCELLED) = 0x800704C7)
1181
+ IntPtr ppsi;
1182
+ dialog.GetResult(out ppsi);
1183
+ var shellItem = (IShellItem)Marshal.GetTypedObjectForIUnknown(ppsi, typeof(IShellItem));
1184
+ string path;
1185
+ shellItem.GetDisplayName(0x80058000, out path); // SIGDN_FILESYSPATH
1186
+ Marshal.Release(ppsi);
1187
+ return path;
1188
+ }
1189
+ }
1190
+ '@
1191
+ Add-Type -TypeDefinition $code
1192
+ $result = [FolderPicker]::Pick("${safeTitle}", "${safeDefaultPath}")
1193
+ if ($result) { Write-Output $result }
1194
+ `;
1195
+ return new Promise((resolve3, reject) => {
1196
+ const child = spawn3("powershell", ["-STA", "-NoProfile", "-WindowStyle", "Hidden", "-Command", psScript], {
1197
+ stdio: ["ignore", "pipe", "pipe"],
1198
+ windowsHide: true
1199
+ });
1200
+ let stdout = "";
1201
+ let stderr = "";
1202
+ child.stdout?.on("data", (d) => {
1203
+ stdout += d.toString();
1204
+ });
1205
+ child.stderr?.on("data", (d) => {
1206
+ stderr += d.toString();
1207
+ });
1208
+ child.on("close", (code) => {
1209
+ if (code === 0) {
1210
+ const selected = stdout.trim();
1211
+ resolve3(selected || null);
1212
+ return;
1213
+ }
1214
+ reject(new Error(`PowerShell \u6587\u4EF6\u5939\u9009\u62E9\u5668\u6267\u884C\u5931\u8D25: ${stderr.trim() || `\u9000\u51FA\u7801 ${code}`}`));
1215
+ });
1216
+ child.on("error", (err) => {
1217
+ reject(new Error(`\u5524\u8D77 PowerShell \u6587\u4EF6\u5939\u9009\u62E9\u5668\u5931\u8D25: ${err.message}`));
1218
+ });
1219
+ });
1220
+ }
1221
+ function pickDirectoryLinux(title, defaultPath) {
1222
+ const args = ["--file-selection", "--directory", `--title=${title}`];
1223
+ if (defaultPath) {
1224
+ args.push(`--filename=${defaultPath}`);
1225
+ }
1226
+ return new Promise((resolve3, reject) => {
1227
+ const child = spawn3("zenity", args, {
1228
+ stdio: ["ignore", "pipe", "pipe"]
1229
+ });
1230
+ let stdout = "";
1231
+ let stderr = "";
1232
+ child.stdout?.on("data", (d) => {
1233
+ stdout += d.toString();
1234
+ });
1235
+ child.stderr?.on("data", (d) => {
1236
+ stderr += d.toString();
1237
+ });
1238
+ child.on("close", (code) => {
1239
+ if (code === 0) {
1240
+ const selected = stdout.trim();
1241
+ resolve3(selected || null);
1242
+ return;
1243
+ }
1244
+ if (code === 1) {
1245
+ resolve3(null);
1246
+ return;
1247
+ }
1248
+ reject(new Error(`zenity \u76EE\u5F55\u9009\u62E9\u5668\u5F02\u5E38\u9000\u51FA (\u9000\u51FA\u7801 ${code}): ${stderr.trim()}`));
1249
+ });
1250
+ child.on("error", (err) => {
1251
+ if (err?.code === "ENOENT") {
1252
+ reject(new Error("\u672A\u68C0\u6D4B\u5230 zenity \u76EE\u5F55\u9009\u62E9\u5668\uFF0C\u8BF7\u5148\u5728\u7CFB\u7EDF\u4E2D\u5B89\u88C5 zenity (\u4F8B\u5982: sudo apt install zenity)"));
1253
+ return;
1254
+ }
1255
+ reject(new Error(`\u5524\u8D77 zenity \u76EE\u5F55\u9009\u62E9\u5668\u5931\u8D25: ${err.message}`));
1256
+ });
1257
+ });
1258
+ }
1259
+ var init_directory_picker = __esm({
1260
+ "src/bridge/directory-picker.ts"() {
1261
+ "use strict";
1262
+ }
1263
+ });
1264
+
1265
+ // src/bridge/bridge-client.ts
1266
+ var bridge_client_exports = {};
1267
+ __export(bridge_client_exports, {
1268
+ BRIDGE_ERROR_CODE: () => BRIDGE_ERROR_CODE,
1269
+ ExtensionBridgeClient: () => ExtensionBridgeClient,
1270
+ PROTOCOL_VERSION: () => PROTOCOL_VERSION,
1271
+ computeHmac: () => computeHmac,
1272
+ deriveSharedSecret: () => deriveSharedSecret,
1273
+ getKillPortHint: () => getKillPortHint,
1274
+ resolveOrCreateAuthToken: () => resolveOrCreateAuthToken,
1275
+ verifyHmacConstantTime: () => verifyHmacConstantTime
1276
+ });
1277
+ import { WebSocketServer, WebSocket as WsClient } from "ws";
1278
+ import { EventEmitter } from "events";
1279
+ import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "crypto";
1280
+ import fs4 from "fs";
1281
+ import path4 from "path";
1282
+ import os3 from "os";
1283
+ function resolveOrCreateAuthToken(explicitToken) {
1284
+ if (explicitToken && explicitToken.trim()) {
1285
+ const token = explicitToken.trim();
1286
+ try {
1287
+ if (!fs4.existsSync(TOKEN_FILE_DIR)) {
1288
+ fs4.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1289
+ }
1290
+ fs4.writeFileSync(TOKEN_FILE_PATH, token, { mode: 384, encoding: "utf8" });
1291
+ } catch {
1292
+ }
1293
+ return token;
1294
+ }
1295
+ if (process.env.ROBOT_WXT_BRIDGE_TOKEN) {
1296
+ return process.env.ROBOT_WXT_BRIDGE_TOKEN.trim();
1297
+ }
1298
+ try {
1299
+ if (fs4.existsSync(TOKEN_FILE_PATH)) {
1300
+ const existing = fs4.readFileSync(TOKEN_FILE_PATH, "utf8").trim();
1301
+ if (existing) return existing;
1302
+ }
1303
+ } catch {
1304
+ }
1305
+ const newToken = randomBytes(32).toString("hex");
1306
+ try {
1307
+ if (!fs4.existsSync(TOKEN_FILE_DIR)) {
1308
+ fs4.mkdirSync(TOKEN_FILE_DIR, { mode: 448, recursive: true });
1309
+ }
1310
+ fs4.writeFileSync(TOKEN_FILE_PATH, newToken, { mode: 384, encoding: "utf8" });
1311
+ } catch {
1312
+ }
1313
+ return newToken;
1314
+ }
1315
+ function parseMajorVersion(v) {
1316
+ return parseInt(v.split(".")[0] ?? "0", 10);
1317
+ }
1318
+ function deriveSharedSecret(tokenOrExtensionId, hmacSalt) {
1319
+ return createHash("sha256").update(`${tokenOrExtensionId}:${hmacSalt}`).digest();
1320
+ }
1321
+ function computeHmac(secret, challenge) {
1322
+ return createHmac("sha256", secret).update(challenge).digest("hex");
1323
+ }
1324
+ function verifyHmacConstantTime(actualHex, expectedHex) {
1325
+ if (!actualHex || typeof actualHex !== "string") return false;
1326
+ try {
1327
+ const actualBuf = Buffer.from(actualHex, "hex");
1328
+ const expectedBuf = Buffer.from(expectedHex, "hex");
1329
+ if (actualBuf.length !== expectedBuf.length) return false;
1330
+ return timingSafeEqual(actualBuf, expectedBuf);
1331
+ } catch {
1332
+ return false;
1333
+ }
1334
+ }
1335
+ function getKillPortHint(port) {
1336
+ if (process.platform === "win32") {
1337
+ return `netstat -ano | findstr :${port} \uFF08\u627E\u5230 PID \u540E\u6267\u884C: taskkill /pid <PID> /T /F\uFF09`;
1338
+ }
1339
+ return `lsof -ti:${port} | xargs kill -9`;
1340
+ }
1341
+ var WS_PORT, REQUEST_TIMEOUT_MS, HANDSHAKE_TIMEOUT_MS, WS_OPEN, TOKEN_FILE_DIR, TOKEN_FILE_PATH, ExtensionBridgeClient;
1342
+ var init_bridge_client = __esm({
1343
+ "src/bridge/bridge-client.ts"() {
1344
+ "use strict";
1345
+ init_protocol();
1346
+ init_bash_executor();
1347
+ init_directory_picker();
1348
+ WS_PORT = DEFAULT_WS_PORT;
1349
+ REQUEST_TIMEOUT_MS = 3e4;
1350
+ HANDSHAKE_TIMEOUT_MS = 5e3;
1351
+ WS_OPEN = 1;
1352
+ TOKEN_FILE_DIR = path4.join(os3.homedir(), ".robot-wxt");
1353
+ TOKEN_FILE_PATH = path4.join(TOKEN_FILE_DIR, "bridge-token");
1354
+ ExtensionBridgeClient = class _ExtensionBridgeClient extends EventEmitter {
1355
+ constructor(port = WS_PORT, authToken) {
1356
+ super();
1357
+ this.port = port;
1358
+ this.explicitAuthToken = authToken;
1359
+ this.authToken = authToken ?? resolveOrCreateAuthToken();
1360
+ }
1361
+ port;
1362
+ wss = null;
1363
+ extensionSocket = null;
1364
+ clientSocket = null;
1365
+ forwardedRequests = /* @__PURE__ */ new Map();
1366
+ pending = /* @__PURE__ */ new Map();
1367
+ socketAuthStates = /* @__PURE__ */ new Map();
1368
+ destroyed = false;
1369
+ idCounter = 0;
1370
+ isClientMode = false;
1371
+ explicitAuthToken;
1372
+ authToken;
1373
+ /** 获取当前服务端使用的认证 Token */
1374
+ get currentAuthToken() {
1375
+ return this.authToken;
1376
+ }
1377
+ /** 获取当前激活的 Socket */
1378
+ _getActiveSocket() {
1379
+ if (this.isClientMode && this.clientSocket) {
1380
+ return this.clientSocket.readyState === WS_OPEN ? this.clientSocket : null;
1381
+ }
1382
+ if (this.extensionSocket && this.extensionSocket.readyState === WS_OPEN) {
1383
+ return this.extensionSocket;
1384
+ }
1385
+ return null;
1386
+ }
1387
+ /** 是否已有就绪的扩展连接(或作为 Client 已连接到 Server) */
1388
+ get isConnected() {
1389
+ return this._getActiveSocket() != null;
1390
+ }
1391
+ /** 等待扩展连接或作为客户端连入服务就绪 */
1392
+ async waitForReady(timeoutMs = 5e3) {
1393
+ if (this.isConnected) return true;
1394
+ return new Promise((resolve3) => {
1395
+ const timer = setTimeout(() => {
1396
+ cleanup();
1397
+ resolve3(this.isConnected);
1398
+ }, timeoutMs);
1399
+ const cleanup = () => {
1400
+ clearTimeout(timer);
1401
+ this.off("connected", onConnected);
1402
+ };
1403
+ const onConnected = () => {
1404
+ cleanup();
1405
+ resolve3(true);
1406
+ };
1407
+ this.on("connected", onConnected);
1408
+ });
1409
+ }
1410
+ /**
1411
+ * 静态探测目标端口的桥接服务状态
1412
+ */
1413
+ static async probe(port = WS_PORT, authToken, timeoutMs = 800) {
1414
+ const client = new _ExtensionBridgeClient(port, authToken);
1415
+ try {
1416
+ const res = await client._probeExistingServer(timeoutMs);
1417
+ if (client.clientSocket) {
1418
+ try {
1419
+ client.clientSocket.close(1e3, "Probe finished");
1420
+ } catch {
1421
+ }
1422
+ }
1423
+ if (res.success) {
1424
+ return { running: true, authed: true };
1425
+ }
1426
+ if (res.error) {
1427
+ const isAuthError = res.error.message.includes("HMAC") || res.error.message.includes("\u8BA4\u8BC1\u5931\u8D25") || res.error.message.includes("Unauthorized");
1428
+ if (isAuthError) {
1429
+ return { running: true, authed: false, error: res.error.message };
1430
+ }
1431
+ return { running: false, authed: false, error: res.error.message };
1432
+ }
1433
+ return { running: false, authed: false };
1434
+ } finally {
1435
+ client.destroy();
1436
+ }
1437
+ }
1438
+ /**
1439
+ * 自动探测模式:
1440
+ * 优先尝试连接已存在的 18999 服务;连接不上时退回启动本机的 WebSocketServer。
1441
+ */
1442
+ async startAuto(probeTimeoutMs = 800) {
1443
+ if (this.destroyed) return;
1444
+ const probe = await this._probeExistingServer(probeTimeoutMs);
1445
+ if (probe.success) {
1446
+ return;
1447
+ }
1448
+ if (probe.error) {
1449
+ throw probe.error;
1450
+ }
1451
+ this.connect();
1452
+ }
1453
+ async _probeExistingServer(timeoutMs) {
1454
+ return new Promise((resolve3) => {
1455
+ let settled = false;
1456
+ let connectionEstablished = false;
1457
+ let authFailedReason = null;
1458
+ const ws = new WsClient(`ws://127.0.0.1:${this.port}`);
1459
+ const timer = setTimeout(() => {
1460
+ if (!settled) {
1461
+ settled = true;
1462
+ try {
1463
+ ws.terminate();
1464
+ } catch {
1465
+ }
1466
+ if (authFailedReason) {
1467
+ resolve3({
1468
+ success: false,
1469
+ error: new Error(
1470
+ `\u672C\u5730\u6865\u63A5\u670D\u52A1\uFF08127.0.0.1:${this.port}\uFF09\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46 HMAC \u8BA4\u8BC1\u5931\u8D25\uFF1A${authFailedReason}\u3002
1471
+ \u8BF7\u786E\u8BA4\u5F53\u524D CLI \u4F7F\u7528\u7684 Token \u4E0E\u8BE5\u670D\u52A1\u4E00\u81F4\uFF0C\u6216\u7EC8\u6B62\u5360\u7528\u8FDB\u7A0B\uFF08\u53EF\u6267\u884C: ${getKillPortHint(this.port)}\uFF09\u3002`
1472
+ )
1473
+ });
1474
+ } else if (connectionEstablished) {
1475
+ resolve3({
1476
+ success: false,
1477
+ error: new Error(
1478
+ `\u672C\u5730 127.0.0.1:${this.port} \u5DF2\u88AB\u5360\u7528\u4E14\u65E0\u54CD\u5E94\uFF08\u53EF\u80FD\u4E0D\u662F\u6709\u6548\u6865\u63A5\u670D\u52A1\uFF09\uFF0C\u8BF7\u5148\u7EC8\u6B62\u5360\u7528\u8FDB\u7A0B\uFF08\u53EF\u6267\u884C: ${getKillPortHint(this.port)}\uFF09\u3002`
1479
+ )
1480
+ });
1481
+ } else {
1482
+ resolve3({ success: false });
1483
+ }
1484
+ }
1485
+ }, timeoutMs);
1486
+ ws.on("open", () => {
1487
+ connectionEstablished = true;
1488
+ });
1489
+ ws.on("close", (code, reason) => {
1490
+ const reasonStr = reason ? reason.toString() : "";
1491
+ if (code === WS_CLOSE_CODE.UNAUTHORIZED || reasonStr.includes("Unauthorized") || reasonStr.includes("HMAC")) {
1492
+ authFailedReason = reasonStr || "Token \u8BA4\u8BC1\u88AB\u62D2\u7EDD (HMAC verification failed)";
1493
+ }
1494
+ if (!settled && authFailedReason) {
1495
+ settled = true;
1496
+ clearTimeout(timer);
1497
+ resolve3({
1498
+ success: false,
1499
+ error: new Error(
1500
+ `\u672C\u5730\u6865\u63A5\u670D\u52A1\uFF08127.0.0.1:${this.port}\uFF09\u6B63\u5728\u8FD0\u884C\uFF0C\u4F46 HMAC \u8BA4\u8BC1\u5931\u8D25\uFF1A${authFailedReason}\u3002
1501
+ \u8BF7\u786E\u8BA4\u5F53\u524D CLI \u4F7F\u7528\u7684 Token \u4E0E\u8BE5\u670D\u52A1\u4E00\u81F4\uFF0C\u6216\u7EC8\u6B62\u5360\u7528\u8FDB\u7A0B\uFF08\u53EF\u6267\u884C: ${getKillPortHint(this.port)}\uFF09\u3002`
1502
+ )
1503
+ });
1504
+ }
1505
+ });
1506
+ ws.on("message", (data) => {
1507
+ try {
1508
+ const msg = JSON.parse(data.toString());
1509
+ if (msg.type === "BRIDGE_CHALLENGE" && msg.challenge && msg.hmacSalt) {
1510
+ connectionEstablished = true;
1511
+ const secret = deriveSharedSecret(this.authToken, msg.hmacSalt);
1512
+ const hmac = computeHmac(secret, msg.challenge);
1513
+ ws.send(
1514
+ JSON.stringify({
1515
+ type: "BRIDGE_AUTH",
1516
+ clientType: "cli-client",
1517
+ challenge: msg.challenge,
1518
+ hmac
1519
+ })
1520
+ );
1521
+ return;
1522
+ }
1523
+ if (msg.type === "BRIDGE_AUTH_OK" && !settled) {
1524
+ settled = true;
1525
+ clearTimeout(timer);
1526
+ this.isClientMode = true;
1527
+ this.clientSocket = ws;
1528
+ this._bindClientSocket(ws);
1529
+ this.emit("connected");
1530
+ resolve3({ success: true });
1531
+ }
1532
+ } catch {
1533
+ }
1534
+ });
1535
+ ws.once("error", (err) => {
1536
+ if (!settled) {
1537
+ settled = true;
1538
+ clearTimeout(timer);
1539
+ try {
1540
+ ws.terminate();
1541
+ } catch {
1542
+ }
1543
+ if (err?.code === "ECONNREFUSED" || err?.message?.includes("ECONNREFUSED")) {
1544
+ resolve3({ success: false });
1545
+ } else {
1546
+ resolve3({
1547
+ success: false,
1548
+ error: new Error(`\u8FDE\u63A5\u672C\u5730 127.0.0.1:${this.port} \u5931\u8D25: ${err?.message || err}`)
1549
+ });
1550
+ }
1551
+ }
1552
+ });
1553
+ });
1554
+ }
1555
+ _bindClientSocket(ws) {
1556
+ ws.on("message", (data) => {
1557
+ let msg;
1558
+ try {
1559
+ msg = JSON.parse(data.toString());
1560
+ } catch {
1561
+ return;
1562
+ }
1563
+ if (msg.type === "BRIDGE_CHALLENGE" || msg.type === "BRIDGE_AUTH_OK") {
1564
+ return;
1565
+ }
1566
+ if (msg.method === "notification/sub_agent_progress") {
1567
+ this.emit("progress", msg.params);
1568
+ return;
1569
+ }
1570
+ const pending = this.pending.get(String(msg.id));
1571
+ if (!pending) return;
1572
+ this.pending.delete(String(msg.id));
1573
+ clearTimeout(pending.timer);
1574
+ if (msg.error) {
1575
+ pending.reject(new Error(`[${msg.error.code}] ${msg.error.message}`));
1576
+ } else {
1577
+ pending.resolve(msg.result);
1578
+ }
1579
+ });
1580
+ ws.on("close", () => {
1581
+ this.clientSocket = null;
1582
+ this.emit("disconnected");
1583
+ this._rejectAllPending("\u4E0E\u670D\u52A1\u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00");
1584
+ });
1585
+ }
1586
+ /** 启动 WebSocket 服务端,等待 Chrome 扩展(Offscreen Document)连入 */
1587
+ connect() {
1588
+ if (this.destroyed) return;
1589
+ if (this.wss) return;
1590
+ try {
1591
+ this.wss = new WebSocketServer({
1592
+ port: this.port,
1593
+ host: "127.0.0.1",
1594
+ verifyClient: (info, callback) => {
1595
+ const origin = info.origin || info.req.headers.origin;
1596
+ if (origin) {
1597
+ const isAllowedExtension = origin.startsWith("chrome-extension://") || origin.startsWith("moz-extension://") || origin.startsWith("extension://");
1598
+ if (!isAllowedExtension) {
1599
+ callback(false, 403, "Forbidden origin: only browser extensions are allowed");
1600
+ return;
1601
+ }
1602
+ }
1603
+ callback(true);
1604
+ }
1605
+ });
1606
+ } catch (err) {
1607
+ this.emit("error", err);
1608
+ return;
1609
+ }
1610
+ this.wss.on("listening", () => {
1611
+ this.emit("listening");
1612
+ });
1613
+ this.wss.on("error", (err) => {
1614
+ if (err?.code === "EADDRINUSE") {
1615
+ process.stderr.write(
1616
+ `[webmcp-cli] \u26A0\uFE0F \u7AEF\u53E3 ${this.port} \u5DF2\u88AB\u5360\u7528\uFF0C\u8BF7\u5148\u7EC8\u6B62\u5360\u7528\u8BE5\u7AEF\u53E3\u7684\u65E7\u8FDB\u7A0B\uFF08\u53EF\u6267\u884C: ${getKillPortHint(this.port)}\uFF09
1617
+ `
1618
+ );
1619
+ }
1620
+ this.emit("error", err);
1621
+ });
1622
+ this.wss.on("connection", (socket) => {
1623
+ const challenge = randomUUID();
1624
+ const hmacSalt = randomBytes(16).toString("hex");
1625
+ const authState = {
1626
+ authenticated: false,
1627
+ challenge,
1628
+ hmacSalt,
1629
+ timer: setTimeout(() => {
1630
+ if (!authState.authenticated) {
1631
+ try {
1632
+ socket.close(WS_CLOSE_CODE.UNAUTHORIZED, "Handshake timeout");
1633
+ } catch {
1634
+ }
1635
+ }
1636
+ }, HANDSHAKE_TIMEOUT_MS)
1637
+ };
1638
+ this.socketAuthStates.set(socket, authState);
1639
+ try {
1640
+ socket.send(
1641
+ JSON.stringify({
1642
+ type: "BRIDGE_CHALLENGE",
1643
+ challenge,
1644
+ hmacSalt,
1645
+ protocolVersion: PROTOCOL_VERSION
1646
+ })
1647
+ );
1648
+ } catch {
1649
+ }
1650
+ socket.on("message", async (data) => {
1651
+ await this.handleSocketMessage(socket, data);
1652
+ });
1653
+ socket.on("close", () => {
1654
+ const auth = this.socketAuthStates.get(socket);
1655
+ if (auth?.timer) clearTimeout(auth.timer);
1656
+ this.socketAuthStates.delete(socket);
1657
+ for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1658
+ if (fwdInfo.socket === socket) {
1659
+ if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
1660
+ this.forwardedRequests.delete(reqId);
1661
+ }
1662
+ }
1663
+ if (socket === this.extensionSocket) {
1664
+ this.extensionSocket = null;
1665
+ this.emit("disconnected");
1666
+ for (const [reqId, fwdInfo] of this.forwardedRequests.entries()) {
1667
+ if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
1668
+ try {
1669
+ if (fwdInfo.socket.readyState === WS_OPEN) {
1670
+ fwdInfo.socket.send(
1671
+ JSON.stringify({
1672
+ jsonrpc: "2.0",
1673
+ id: fwdInfo.originalId,
1674
+ error: {
1675
+ code: BRIDGE_ERROR_CODE.EXTENSION_NOT_CONNECTED,
1676
+ message: "Chrome \u6269\u5C55\u8FDE\u63A5\u5DF2\u65AD\u5F00"
1677
+ }
1678
+ })
1679
+ );
1680
+ }
1681
+ } catch {
1682
+ }
1683
+ this.forwardedRequests.delete(reqId);
1684
+ }
1685
+ this._rejectAllPending("WebSocket \u8FDE\u63A5\u5DF2\u65AD\u5F00");
1686
+ }
1687
+ });
1688
+ socket.on("error", () => {
1689
+ });
1690
+ });
1691
+ this.wss.on("error", (err) => {
1692
+ this.emit("error", err);
1693
+ });
1694
+ }
1695
+ async handleSocketMessage(socket, data) {
1696
+ let msg;
1697
+ try {
1698
+ msg = JSON.parse(data.toString());
1699
+ } catch {
1700
+ return;
1701
+ }
1702
+ const currentAuth = this.socketAuthStates.get(socket);
1703
+ if (!currentAuth || !currentAuth.authenticated) {
1704
+ await this.handleHandshake(socket, currentAuth, msg);
1705
+ return;
1706
+ }
1707
+ if (msg.type === "KEEP_ALIVE" || msg.type === "WS_BRIDGE_KEEP_ALIVE") {
1708
+ return;
1709
+ }
1710
+ if (msg.method && msg.id) {
1711
+ if (socket === this.extensionSocket) {
1712
+ await this.handleExtensionSystemRpc(socket, msg);
1713
+ return;
1714
+ }
1715
+ await this.handleForwardRpc(socket, msg);
1716
+ return;
1717
+ }
1718
+ if (msg.method && !msg.id) {
1719
+ this.handleProgressNotification(msg);
1720
+ return;
1721
+ }
1722
+ if (msg.id) {
1723
+ this.handleIncomingResponse(msg);
1724
+ }
1725
+ }
1726
+ verifyHandshakeHmac(currentAuth, hmac) {
1727
+ if (!currentAuth.hmacSalt || !hmac) return false;
1728
+ const secret = deriveSharedSecret(this.authToken, currentAuth.hmacSalt);
1729
+ const expectedHmac = computeHmac(secret, currentAuth.challenge);
1730
+ if (verifyHmacConstantTime(hmac, expectedHmac)) {
1731
+ return true;
1732
+ }
1733
+ if (!this.explicitAuthToken) {
1734
+ const diskToken = resolveOrCreateAuthToken();
1735
+ if (diskToken && diskToken !== this.authToken) {
1736
+ const diskSecret = deriveSharedSecret(diskToken, currentAuth.hmacSalt);
1737
+ const diskExpectedHmac = computeHmac(diskSecret, currentAuth.challenge);
1738
+ if (verifyHmacConstantTime(hmac, diskExpectedHmac)) {
1739
+ this.authToken = diskToken;
1740
+ return true;
1741
+ }
1742
+ }
1743
+ }
1744
+ return false;
1745
+ }
1746
+ async handleHandshake(socket, currentAuth, msg) {
1747
+ if (!currentAuth || msg.type !== "BRIDGE_AUTH" || msg.challenge !== currentAuth.challenge) {
1748
+ try {
1749
+ socket.close(WS_CLOSE_CODE.UNAUTHORIZED, "Unauthorized: Handshake failed");
1750
+ } catch {
1751
+ }
1752
+ return;
1753
+ }
1754
+ const clientVersion = typeof msg.protocolVersion === "string" ? msg.protocolVersion : PROTOCOL_VERSION;
1755
+ if (parseMajorVersion(clientVersion) !== parseMajorVersion(PROTOCOL_VERSION)) {
1756
+ try {
1757
+ socket.close(
1758
+ WS_CLOSE_CODE.PROTOCOL_MISMATCH,
1759
+ `Protocol version mismatch: server=${PROTOCOL_VERSION}, client=${clientVersion}`
1760
+ );
1761
+ } catch {
1762
+ }
1763
+ return;
1764
+ }
1765
+ const verified = this.verifyHandshakeHmac(currentAuth, msg.hmac);
1766
+ if (!verified) {
1767
+ const tokenFingerprint = this.authToken ? `${this.authToken.slice(0, 6)}...` : "none";
1768
+ process.stderr.write(
1769
+ `[webmcp-cli] HMAC \u9A8C\u8BC1\u5931\u8D25\uFF0C\u62D2\u7EDD\u8FDE\u63A5. client extensionId=${msg.extensionId}, server authTokenFingerprint=${tokenFingerprint}
1770
+ `
1771
+ );
1772
+ try {
1773
+ socket.close(WS_CLOSE_CODE.UNAUTHORIZED, "Unauthorized: HMAC verification failed");
1774
+ } catch {
1775
+ }
1776
+ return;
1777
+ }
1778
+ clearTimeout(currentAuth.timer);
1779
+ currentAuth.authenticated = true;
1780
+ currentAuth.clientType = msg.clientType;
1781
+ if (msg.clientType === "extension") {
1782
+ if (this.extensionSocket && this.extensionSocket !== socket) {
1783
+ try {
1784
+ this.extensionSocket.close(WS_CLOSE_CODE.REPLACED, "Replaced by new connection");
1785
+ } catch {
1786
+ }
1787
+ }
1788
+ this.extensionSocket = socket;
1789
+ this.emit("connected");
1790
+ }
1791
+ try {
1792
+ socket.send(
1793
+ JSON.stringify({
1794
+ type: "BRIDGE_AUTH_OK",
1795
+ clientType: msg.clientType
1796
+ })
1797
+ );
1798
+ } catch {
1799
+ }
1800
+ }
1801
+ async handleForwardRpc(socket, msg) {
1802
+ let targetSocket = this._getActiveSocket();
1803
+ if (!targetSocket) {
1804
+ await new Promise((resolve3) => {
1805
+ const timer2 = setTimeout(resolve3, 6e3);
1806
+ this.once("connected", () => {
1807
+ clearTimeout(timer2);
1808
+ resolve3();
1809
+ });
1810
+ });
1811
+ targetSocket = this._getActiveSocket();
1812
+ }
1813
+ if (!targetSocket) {
1814
+ socket.send(
1815
+ JSON.stringify({
1816
+ jsonrpc: "2.0",
1817
+ id: msg.id,
1818
+ error: {
1819
+ code: BRIDGE_ERROR_CODE.EXTENSION_NOT_CONNECTED,
1820
+ message: "Chrome \u6269\u5C55\u5C1A\u672A\u8FDE\u63A5\u5230\u672C\u5730\u6865\u63A5\u670D\u52A1\u3002\u8BF7\u786E\u4FDD\u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u4E14\u5728\u4FA7\u8FB9\u680F\u4E2D\u5F00\u542F\u4E86\u300C\u672C\u5730 WebSocket \u6865\u63A5\u300D\u5F00\u5173\u3002"
1821
+ }
1822
+ })
1823
+ );
1824
+ return;
1825
+ }
1826
+ const internalId = `fwd_${++this.idCounter}_${Date.now()}`;
1827
+ const timeoutMs = msg.method === "sub_agent/run" ? 18e4 : REQUEST_TIMEOUT_MS;
1828
+ const timer = setTimeout(() => {
1829
+ const fwd = this.forwardedRequests.get(internalId);
1830
+ if (!fwd) return;
1831
+ this.forwardedRequests.delete(internalId);
1832
+ try {
1833
+ if (fwd.socket.readyState === WS_OPEN) {
1834
+ fwd.socket.send(
1835
+ JSON.stringify({
1836
+ jsonrpc: "2.0",
1837
+ id: fwd.originalId,
1838
+ error: {
1839
+ code: BRIDGE_ERROR_CODE.REQUEST_TIMEOUT,
1840
+ message: `\u8BF7\u6C42\u8F6C\u53D1\u81F3 Chrome \u6269\u5C55\u8D85\u65F6 (${timeoutMs / 1e3}s)`
1841
+ }
1842
+ })
1843
+ );
1844
+ }
1845
+ } catch {
1846
+ }
1847
+ }, timeoutMs);
1848
+ this.forwardedRequests.set(internalId, {
1849
+ socket,
1850
+ originalId: msg.id,
1851
+ progressToken: msg.params?.progressToken,
1852
+ timer
1853
+ });
1854
+ const outgoingMsg = { ...msg, id: internalId };
1855
+ targetSocket.send(JSON.stringify(outgoingMsg));
1856
+ }
1857
+ sendJsonRpcResponse(socket, response) {
1858
+ if (socket.readyState === WS_OPEN) {
1859
+ socket.send(JSON.stringify({ jsonrpc: "2.0", ...response }));
1860
+ }
1861
+ }
1862
+ formatSystemRpcError(err) {
1863
+ if (err instanceof BashInvalidParamsError || err?.code === "INVALID_PARAMS" || err?.message === "\u547D\u4EE4\u4E0D\u80FD\u4E3A\u7A7A") {
1864
+ return { code: BRIDGE_ERROR_CODE.INVALID_PARAMS, message: err?.message || "\u53C2\u6570\u9519\u8BEF" };
1865
+ }
1866
+ if (err instanceof BashTimeoutError || err?.code === "TIMEOUT" || typeof err?.message === "string" && err.message.includes("\u8D85\u65F6")) {
1867
+ return { code: BRIDGE_ERROR_CODE.EXECUTION_TIMEOUT, message: err?.message || "\u547D\u4EE4\u6267\u884C\u8D85\u65F6" };
1868
+ }
1869
+ return { code: BRIDGE_ERROR_CODE.EXECUTION_FAILED, message: err?.message || "\u6267\u884C\u5931\u8D25" };
1870
+ }
1871
+ systemRpcHandlers = Object.assign(
1872
+ /* @__PURE__ */ Object.create(null),
1873
+ {
1874
+ [BRIDGE_METHODS.SYSTEM_BASH]: async (params) => {
1875
+ return executeBash(params || {});
1876
+ },
1877
+ [BRIDGE_METHODS.SYSTEM_PICK_DIRECTORY]: async (params) => {
1878
+ const selectedPath = await pickDirectory({
1879
+ title: params?.title || void 0,
1880
+ defaultPath: params?.defaultPath || void 0
1881
+ });
1882
+ return {
1883
+ path: selectedPath,
1884
+ selectedPath,
1885
+ canceled: selectedPath === null
1886
+ };
1887
+ }
1888
+ }
1889
+ );
1890
+ async handleExtensionSystemRpc(socket, msg) {
1891
+ const { id, method, params } = msg;
1892
+ const handler = typeof method === "string" && Object.prototype.hasOwnProperty.call(this.systemRpcHandlers, method) ? this.systemRpcHandlers[method] : void 0;
1893
+ if (!handler || typeof handler !== "function") {
1894
+ this.sendJsonRpcResponse(socket, {
1895
+ id,
1896
+ error: {
1897
+ code: BRIDGE_ERROR_CODE.METHOD_NOT_FOUND,
1898
+ message: `\u4E0D\u652F\u6301\u7684\u65B9\u6CD5: ${method}`
1899
+ }
1900
+ });
1901
+ return;
1902
+ }
1903
+ try {
1904
+ const result = await handler(params);
1905
+ this.sendJsonRpcResponse(socket, { id, result });
1906
+ } catch (err) {
1907
+ this.sendJsonRpcResponse(socket, { id, error: this.formatSystemRpcError(err) });
1908
+ }
1909
+ }
1910
+ handleProgressNotification(msg) {
1911
+ const token = msg.params?.progressToken;
1912
+ if (token != null) {
1913
+ for (const [, reqInfo] of this.forwardedRequests) {
1914
+ if (reqInfo.progressToken === token) {
1915
+ try {
1916
+ if (reqInfo.socket.readyState === WS_OPEN) {
1917
+ reqInfo.socket.send(JSON.stringify(msg));
1918
+ }
1919
+ } catch {
1920
+ }
1921
+ }
1922
+ }
1923
+ }
1924
+ this.emit("progress", msg.params);
1925
+ }
1926
+ handleIncomingResponse(msg) {
1927
+ const fwdInfo = this.forwardedRequests.get(String(msg.id));
1928
+ if (fwdInfo) {
1929
+ if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
1930
+ this.forwardedRequests.delete(String(msg.id));
1931
+ try {
1932
+ if (fwdInfo.socket.readyState === WS_OPEN) {
1933
+ fwdInfo.socket.send(
1934
+ JSON.stringify({
1935
+ ...msg,
1936
+ id: fwdInfo.originalId
1937
+ })
1938
+ );
1939
+ }
1940
+ } catch {
1941
+ }
1942
+ return;
1943
+ }
1944
+ const pending = this.pending.get(String(msg.id));
1945
+ if (!pending) return;
1946
+ this.pending.delete(String(msg.id));
1947
+ clearTimeout(pending.timer);
1948
+ if (msg.error) {
1949
+ pending.reject(new Error(`[${msg.error.code}] ${msg.error.message}`));
1950
+ } else {
1951
+ pending.resolve(msg.result);
1952
+ }
1953
+ }
1954
+ /** 发送 JSON-RPC 2.0 请求,返回 result */
1955
+ async call(method, params = {}, timeoutMs, progressToken, autoWaitMs = 0) {
1956
+ if (autoWaitMs > 0 && !this.isConnected) {
1957
+ await this.waitForReady(autoWaitMs);
1958
+ }
1959
+ const targetSocket = this._getActiveSocket();
1960
+ if (!targetSocket) {
1961
+ throw new Error(
1962
+ "\u4E0E Chrome \u6269\u5C55\u7684\u8FDE\u63A5\u5C1A\u672A\u5C31\u7EEA\uFF0C\u8BF7\u786E\u4FDD\uFF1A\n 1. Chrome \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\n 2. Tiny Robot \u6269\u5C55\u5DF2\u5B89\u88C5\u5E76\u5F00\u542F\n 3. \u6269\u5C55\u4FA7\u8FB9\u680F\u5DF2\u6253\u5F00"
1963
+ );
1964
+ }
1965
+ const effectiveTimeoutMs = timeoutMs ?? (method === "sub_agent/run" ? 18e4 : REQUEST_TIMEOUT_MS);
1966
+ const id = String(++this.idCounter);
1967
+ const requestParams = { ...params };
1968
+ if (progressToken != null) {
1969
+ requestParams.progressToken = progressToken;
1970
+ }
1971
+ const request = { jsonrpc: "2.0", id, method, params: requestParams };
1972
+ return new Promise((resolve3, reject) => {
1973
+ const timer = setTimeout(() => {
1974
+ this.pending.delete(id);
1975
+ reject(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF08${effectiveTimeoutMs / 1e3}s\uFF09\uFF1A${method}`));
1976
+ }, effectiveTimeoutMs);
1977
+ this.pending.set(id, { resolve: resolve3, reject, timer });
1978
+ try {
1979
+ targetSocket.send(JSON.stringify(request));
1980
+ } catch (err) {
1981
+ this.pending.delete(id);
1982
+ clearTimeout(timer);
1983
+ reject(err instanceof Error ? err : new Error(String(err)));
1984
+ }
1985
+ });
1986
+ }
1987
+ /** 销毁服务,断开所有连接 */
1988
+ destroy() {
1989
+ this.destroyed = true;
1990
+ this._rejectAllPending("ExtensionBridgeClient \u5DF2\u9500\u6BC1");
1991
+ this.forwardedRequests.clear();
1992
+ if (this.clientSocket) {
1993
+ try {
1994
+ this.clientSocket.close();
1995
+ } catch {
1996
+ }
1997
+ this.clientSocket = null;
1998
+ }
1999
+ if (this.extensionSocket) {
2000
+ try {
2001
+ this.extensionSocket.close();
2002
+ } catch {
2003
+ }
2004
+ this.extensionSocket = null;
2005
+ }
2006
+ if (this.wss) {
2007
+ try {
2008
+ this.wss.close();
2009
+ } catch {
2010
+ }
2011
+ this.wss = null;
2012
+ }
2013
+ }
2014
+ _rejectAllPending(reason) {
2015
+ for (const [, pending] of this.pending) {
2016
+ clearTimeout(pending.timer);
2017
+ pending.reject(new Error(reason));
2018
+ }
2019
+ this.pending.clear();
2020
+ for (const [, fwdInfo] of this.forwardedRequests) {
2021
+ if (fwdInfo.timer) clearTimeout(fwdInfo.timer);
2022
+ }
2023
+ this.forwardedRequests.clear();
2024
+ }
2025
+ };
2026
+ }
2027
+ });
2028
+
2029
+ // src/adapters/wxt-adapter.ts
2030
+ var wxt_adapter_exports = {};
2031
+ __export(wxt_adapter_exports, {
2032
+ WxtBrowserAdapter: () => WxtBrowserAdapter
2033
+ });
2034
+ var AUTO_WAIT_MS, WxtBrowserAdapter;
2035
+ var init_wxt_adapter = __esm({
2036
+ "src/adapters/wxt-adapter.ts"() {
2037
+ "use strict";
2038
+ init_bridge_client();
2039
+ init_protocol();
2040
+ AUTO_WAIT_MS = 6e3;
2041
+ WxtBrowserAdapter = class {
2042
+ client;
2043
+ constructor(token, port) {
2044
+ this.client = new ExtensionBridgeClient(port, token);
2045
+ this.client.on("error", () => {
2046
+ });
2047
+ }
2048
+ /** 确保 WS 桥接服务已启动并连接 */
2049
+ async ensureConnected() {
2050
+ await this.client.startAuto();
2051
+ }
2052
+ async getState(tabId) {
2053
+ const numTabId = typeof tabId === "string" ? parseInt(tabId, 10) || void 0 : tabId;
2054
+ const result = await this.client.call(
2055
+ BRIDGE_METHODS.BROWSER_STATE,
2056
+ { tabId: numTabId },
2057
+ void 0,
2058
+ void 0,
2059
+ AUTO_WAIT_MS
2060
+ );
2061
+ return result;
2062
+ }
2063
+ async callTool(toolName, args, tabId) {
2064
+ const numTabId = typeof tabId === "string" ? parseInt(tabId, 10) || void 0 : tabId;
2065
+ return this.client.call(
2066
+ BRIDGE_METHODS.TOOL_CALL,
2067
+ { toolName, toolArgs: args, tabId: numTabId },
2068
+ void 0,
2069
+ void 0,
2070
+ AUTO_WAIT_MS
2071
+ );
2072
+ }
2073
+ async tabs(action, options) {
2074
+ const numTabId = typeof options.tabId === "string" ? parseInt(options.tabId, 10) || void 0 : options.tabId;
2075
+ return this.client.call(
2076
+ BRIDGE_METHODS.BROWSER_TABS,
2077
+ { tabAction: action, ...options, tabId: numTabId },
2078
+ void 0,
2079
+ void 0,
2080
+ AUTO_WAIT_MS
2081
+ );
2082
+ }
2083
+ /** 委托扩展内 Tiny Robot 子代理闭环执行高层任务 */
2084
+ async runSubAgent(instruction, options = {}, timeoutMs) {
2085
+ const params = { instruction, tabId: options.tabId, maxSteps: options.maxSteps };
2086
+ return this.client.call(
2087
+ BRIDGE_METHODS.SUB_AGENT_RUN,
2088
+ params,
2089
+ timeoutMs,
2090
+ options.progressToken,
2091
+ AUTO_WAIT_MS
2092
+ );
2093
+ }
2094
+ /** 获取已注册的 Skills 列表(L1 索引) */
2095
+ async listSkills() {
2096
+ return this.client.call(BRIDGE_METHODS.SKILLS_LIST, {}, void 0, void 0, AUTO_WAIT_MS);
2097
+ }
2098
+ /** 读取指定 Skill 的详细指令(L2 详情) */
2099
+ async getSkill(name) {
2100
+ return this.client.call(BRIDGE_METHODS.SKILLS_GET, { name }, void 0, void 0, AUTO_WAIT_MS);
2101
+ }
2102
+ /** 读取 Skill 关联资源(L3 资源) */
2103
+ async readSkillResource(name, resourcePath) {
2104
+ return this.client.call(
2105
+ BRIDGE_METHODS.SKILLS_READ_RESOURCE,
2106
+ { name, resourcePath },
2107
+ void 0,
2108
+ void 0,
2109
+ AUTO_WAIT_MS
2110
+ );
2111
+ }
2112
+ /** 获取底层 ExtensionBridgeClient(供 MCP Server 绑定事件) */
2113
+ get bridgeClient() {
2114
+ return this.client;
2115
+ }
2116
+ async dispose() {
2117
+ this.client.destroy();
2118
+ }
2119
+ };
2120
+ }
2121
+ });
2122
+
2123
+ // src/bridge/daemon.ts
2124
+ var daemon_exports = {};
2125
+ __export(daemon_exports, {
2126
+ PID_FILE: () => PID_FILE,
2127
+ ensureBridgeDaemon: () => ensureBridgeDaemon,
2128
+ findPidByPort: () => findPidByPort,
2129
+ getDaemonPidFile: () => getDaemonPidFile,
2130
+ isBridgeDaemonProcess: () => isBridgeDaemonProcess,
2131
+ isDaemonProcessAlive: () => isDaemonProcessAlive,
2132
+ isProcessAlive: () => isProcessAlive,
2133
+ killProcessOnPort: () => killProcessOnPort,
2134
+ killProcessTree: () => killProcessTree2,
2135
+ probeServerReady: () => probeServerReady,
2136
+ runDaemonProcess: () => runDaemonProcess,
2137
+ stopBridgeDaemon: () => stopBridgeDaemon
2138
+ });
2139
+ import { spawn as spawn4, exec } from "child_process";
2140
+ import { promisify } from "util";
2141
+ import fs5 from "fs";
2142
+ import path5 from "path";
2143
+ import os4 from "os";
2144
+ import { fileURLToPath as fileURLToPath3 } from "url";
2145
+ function getDaemonPidFile(port = WS_PORT2) {
2146
+ if (port === WS_PORT2) {
2147
+ return path5.join(DAEMON_DIR, "daemon.pid");
2148
+ }
2149
+ return path5.join(DAEMON_DIR, `daemon-${port}.pid`);
2150
+ }
2151
+ function isProcessAlive(pid) {
2152
+ if (!pid || pid <= 0) return false;
2153
+ try {
2154
+ process.kill(pid, 0);
2155
+ return true;
2156
+ } catch {
2157
+ return false;
2158
+ }
2159
+ }
2160
+ async function findPidByPort(port, excludePid) {
2161
+ if (process.platform === "win32") {
2162
+ try {
2163
+ const { stdout } = await execAsync("netstat -ano -p tcp");
2164
+ const lines = stdout.split(/\r?\n/);
2165
+ for (const rawLine of lines) {
2166
+ const line = rawLine.trim();
2167
+ if (!line.toUpperCase().startsWith("TCP")) continue;
2168
+ const parts = line.split(/\s+/);
2169
+ if (parts.length < 5) continue;
2170
+ const localAddr = parts[1];
2171
+ const state = parts[3]?.toUpperCase() || "";
2172
+ const pid = parseInt(parts[parts.length - 1], 10);
2173
+ if (!pid || isNaN(pid) || excludePid && pid === excludePid) continue;
2174
+ if (localAddr.endsWith(`:${port}`)) {
2175
+ if (state.includes("LISTEN") || state.includes("\u4FA6\u542C")) {
2176
+ return pid;
2177
+ }
2178
+ }
2179
+ }
2180
+ return null;
2181
+ } catch {
2182
+ return null;
2183
+ }
2184
+ }
2185
+ try {
2186
+ const { stdout } = await execAsync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -t`);
2187
+ const pids = stdout.trim().split("\n").map((p) => parseInt(p.trim(), 10)).filter((p) => Boolean(p && !isNaN(p) && (!excludePid || p !== excludePid)));
2188
+ return pids[0] ?? null;
2189
+ } catch {
2190
+ return null;
2191
+ }
2192
+ }
2193
+ async function killProcessTree2(pid) {
2194
+ if (!pid || pid <= 0 || pid === process.pid) return false;
2195
+ if (process.platform === "win32") {
2196
+ try {
2197
+ await execAsync(`taskkill /pid ${pid} /T /F`);
2198
+ } catch {
2199
+ try {
2200
+ process.kill(pid, "SIGKILL");
2201
+ } catch {
2202
+ }
2203
+ }
2204
+ for (let i = 0; i < 15; i++) {
2205
+ await new Promise((r) => setTimeout(r, 100));
2206
+ if (!isProcessAlive(pid)) return true;
2207
+ }
2208
+ return !isProcessAlive(pid);
2209
+ }
2210
+ try {
2211
+ process.kill(pid, "SIGTERM");
2212
+ } catch {
2213
+ }
2214
+ for (let i = 0; i < 10; i++) {
2215
+ await new Promise((r) => setTimeout(r, 100));
2216
+ if (!isProcessAlive(pid)) return true;
2217
+ }
2218
+ try {
2219
+ process.kill(pid, "SIGKILL");
2220
+ } catch {
2221
+ }
2222
+ for (let i = 0; i < 5; i++) {
2223
+ await new Promise((r) => setTimeout(r, 100));
2224
+ if (!isProcessAlive(pid)) return true;
2225
+ }
2226
+ return !isProcessAlive(pid);
2227
+ }
2228
+ async function killProcessOnPort(port) {
2229
+ const pid = await findPidByPort(port, process.pid);
2230
+ if (pid) {
2231
+ await killProcessTree2(pid);
2232
+ for (let i = 0; i < 20; i++) {
2233
+ await new Promise((r) => setTimeout(r, 100));
2234
+ const remainingPid = await findPidByPort(port, process.pid);
2235
+ if (!remainingPid) return true;
2236
+ }
2237
+ }
2238
+ return false;
2239
+ }
2240
+ async function probeServerReady(port = WS_PORT2, token, timeoutMs = 1e3) {
2241
+ return ExtensionBridgeClient.probe(port, token ?? resolveOrCreateAuthToken(), timeoutMs);
2242
+ }
2243
+ function isDaemonProcessAlive(port = WS_PORT2) {
2244
+ const pidFile = getDaemonPidFile(port);
2245
+ if (!fs5.existsSync(pidFile)) return false;
2246
+ try {
2247
+ const pid = parseInt(fs5.readFileSync(pidFile, "utf8").trim(), 10);
2248
+ return Boolean(pid && isProcessAlive(pid));
2249
+ } catch {
2250
+ return false;
2251
+ }
2252
+ }
2253
+ async function ensureBridgeDaemon(options = {}) {
2254
+ const port = options.port ?? WS_PORT2;
2255
+ const token = options.token ?? resolveOrCreateAuthToken();
2256
+ const timeoutMs = options.timeoutMs ?? 6e3;
2257
+ const probe = await probeServerReady(port, token, 800);
2258
+ if (probe.running && probe.authed) {
2259
+ return { newlyStarted: false };
2260
+ }
2261
+ if (probe.running && !probe.authed) {
2262
+ if (!options.silent) {
2263
+ console.log(
2264
+ `\x1B[33m[webmcp-cli]\x1B[0m \u{1F504} \u68C0\u6D4B\u5230\u540E\u53F0\u6865\u63A5\u670D\u52A1 Token \u4E0D\u5339\u914D\uFF0C\u6B63\u5728\u9A8C\u8BC1\u65E7\u5B88\u62A4\u8FDB\u7A0B\u5F52\u5C5E\u5E76\u5C1D\u8BD5\u81EA\u52A8\u91CD\u542F...`
2265
+ );
2266
+ }
2267
+ const stopped = await stopBridgeDaemon(port, { onlyVerified: true });
2268
+ if (!stopped) {
2269
+ const killTip = getKillPortHint(port);
2270
+ throw new Error(
2271
+ `\u672C\u5730 127.0.0.1:${port} \u6B63\u5728\u8FD0\u884C\u670D\u52A1\uFF0C\u4F46\u8BA4\u8BC1\u672A\u901A\u8FC7\uFF08${probe.error || "Token \u4E0D\u5339\u914D"}\uFF09\u3002
2272
+ \u672A\u80FD\u786E\u8BA4\u8BE5\u8FDB\u7A0B\u5F52\u5C5E\u4E8E\u53D7\u7BA1\u7684\u6865\u63A5\u5B88\u62A4\u8FDB\u7A0B\uFF0C\u4E3A\u5B89\u5168\u8D77\u89C1\u5DF2\u963B\u6B62\u81EA\u52A8\u5F3A\u6740\u3002\u8BF7\u68C0\u67E5\u7AEF\u53E3 ${port} \u662F\u5426\u88AB\u5176\u4ED6\u5E94\u7528\u5360\u7528\uFF0C\u6216\u624B\u52A8\u7EC8\u6B62\uFF08\u53EF\u6267\u884C: ${killTip}\uFF09\u3002`
2273
+ );
2274
+ }
2275
+ await new Promise((r) => setTimeout(r, 400));
2276
+ }
2277
+ if (!options.silent) {
2278
+ console.log(`\x1B[36m[webmcp-cli]\x1B[0m \u{1F680} \u672C\u5730\u6865\u63A5\u670D\u52A1\u672A\u8FD0\u884C\uFF0C\u6B63\u5728\u81EA\u52A8\u62C9\u8D77\u5E38\u9A7B\u670D\u52A1 (127.0.0.1:${port})...`);
2279
+ }
2280
+ startDaemonProcessBackground(token, port);
2281
+ const startTime = Date.now();
2282
+ while (Date.now() - startTime < timeoutMs) {
2283
+ await new Promise((r) => setTimeout(r, 200));
2284
+ const check = await probeServerReady(port, token, 400);
2285
+ if (check.running && check.authed) {
2286
+ if (!options.silent) {
2287
+ console.log(`\x1B[32m[webmcp-cli]\x1B[0m \u2705 \u6865\u63A5\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u7B49\u5F85 Chrome \u6269\u5C55\u8FDE\u63A5...`);
2288
+ }
2289
+ return { newlyStarted: true };
2290
+ }
2291
+ }
2292
+ throw new Error(`\u542F\u52A8\u540E\u53F0 WebSocket \u6865\u63A5\u670D\u52A1\u8D85\u65F6\uFF08${timeoutMs}ms\uFF09\uFF0C\u8BF7\u68C0\u67E5\u7AEF\u53E3 ${port} \u662F\u5426\u53EF\u7528\u3002`);
2293
+ }
2294
+ function resolveCliEntry() {
2295
+ const currentDir = path5.dirname(fileURLToPath3(import.meta.url));
2296
+ const distCandidate = path5.join(currentDir, "bin.js");
2297
+ if (fs5.existsSync(distCandidate)) return distCandidate;
2298
+ const projectDistCandidate = path5.resolve(currentDir, "../dist/bin.js");
2299
+ if (fs5.existsSync(projectDistCandidate)) return projectDistCandidate;
2300
+ return distCandidate;
2301
+ }
2302
+ function startDaemonProcessBackground(token, port) {
2303
+ const cliEntry = resolveCliEntry();
2304
+ if (!fs5.existsSync(DAEMON_DIR)) {
2305
+ fs5.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
2306
+ }
2307
+ const child = spawn4(process.execPath, [cliEntry, "--mode", "wxt", "daemon", "--port", String(port)], {
2308
+ detached: true,
2309
+ stdio: "ignore",
2310
+ windowsHide: true,
2311
+ env: {
2312
+ ...process.env,
2313
+ ROBOT_WXT_BRIDGE_TOKEN: token
2314
+ }
2315
+ });
2316
+ child.unref();
2317
+ }
2318
+ async function runDaemonProcess(args) {
2319
+ let port = WS_PORT2;
2320
+ let token;
2321
+ const portIdx = args.indexOf("--port");
2322
+ if (portIdx !== -1 && args[portIdx + 1]) {
2323
+ port = parseInt(args[portIdx + 1], 10);
2324
+ }
2325
+ const tokenIdx = args.indexOf("--token");
2326
+ if (tokenIdx !== -1 && args[tokenIdx + 1]) {
2327
+ token = args[tokenIdx + 1];
2328
+ } else if (process.env.ROBOT_WXT_BRIDGE_TOKEN) {
2329
+ token = process.env.ROBOT_WXT_BRIDGE_TOKEN;
2330
+ }
2331
+ const client = new ExtensionBridgeClient(port, token);
2332
+ client.on("error", (err) => {
2333
+ if (err && (err.code === "EADDRINUSE" || err.message?.includes("EADDRINUSE"))) {
2334
+ process.exit(1);
2335
+ }
2336
+ });
2337
+ const pidFile = getDaemonPidFile(port);
2338
+ client.on("listening", () => {
2339
+ try {
2340
+ if (!fs5.existsSync(DAEMON_DIR)) {
2341
+ fs5.mkdirSync(DAEMON_DIR, { mode: 448, recursive: true });
2342
+ }
2343
+ fs5.writeFileSync(pidFile, String(process.pid), { mode: 384, encoding: "utf8" });
2344
+ } catch {
2345
+ }
2346
+ });
2347
+ client.connect();
2348
+ const shutdown = () => {
2349
+ try {
2350
+ if (fs5.existsSync(pidFile)) {
2351
+ const recordedPid = fs5.readFileSync(pidFile, "utf8").trim();
2352
+ if (recordedPid === String(process.pid)) {
2353
+ fs5.unlinkSync(pidFile);
2354
+ }
2355
+ }
2356
+ } catch {
2357
+ }
2358
+ client.destroy();
2359
+ process.exit(0);
2360
+ };
2361
+ process.on("SIGINT", shutdown);
2362
+ process.on("SIGTERM", shutdown);
2363
+ process.on("exit", shutdown);
2364
+ setInterval(() => {
2365
+ }, 6e4);
2366
+ }
2367
+ async function isBridgeDaemonProcess(pid) {
2368
+ if (!pid || pid <= 0 || !isProcessAlive(pid)) return false;
2369
+ try {
2370
+ let cmd = "";
2371
+ if (process.platform === "win32") {
2372
+ const { stdout } = await execAsync(
2373
+ `powershell -NoProfile -Command "(Get-CimInstance Win32_Process -Filter \\"ProcessId = ${pid}\\").CommandLine"`
2374
+ );
2375
+ cmd = stdout.trim();
2376
+ } else {
2377
+ const { stdout } = await execAsync(`ps -p ${pid} -o command=`);
2378
+ cmd = stdout.trim();
2379
+ }
2380
+ if (!cmd) return false;
2381
+ return (cmd.includes("webmcp-cli") || cmd.includes("bin.js") || cmd.includes("daemon.js")) && cmd.includes("daemon");
2382
+ } catch {
2383
+ return false;
2384
+ }
2385
+ }
2386
+ async function stopBridgeDaemon(port = WS_PORT2, options = {}) {
2387
+ const onlyVerified = options.onlyVerified ?? false;
2388
+ let killedAny = false;
2389
+ const pidFile = getDaemonPidFile(port);
2390
+ let daemonPid = null;
2391
+ if (fs5.existsSync(pidFile)) {
2392
+ try {
2393
+ const pidStr = fs5.readFileSync(pidFile, "utf8").trim();
2394
+ const p = parseInt(pidStr, 10);
2395
+ if (p && isProcessAlive(p)) {
2396
+ daemonPid = p;
2397
+ }
2398
+ } catch {
2399
+ }
2400
+ }
2401
+ if (onlyVerified) {
2402
+ if (!daemonPid) {
2403
+ return false;
2404
+ }
2405
+ const isVerified = await isBridgeDaemonProcess(daemonPid);
2406
+ if (!isVerified) {
2407
+ return false;
2408
+ }
2409
+ const portPid = await findPidByPort(port, process.pid);
2410
+ if (portPid && portPid !== daemonPid) {
2411
+ return false;
2412
+ }
2413
+ const killed = await killProcessTree2(daemonPid);
2414
+ if (killed) {
2415
+ killedAny = true;
2416
+ try {
2417
+ if (fs5.existsSync(pidFile)) fs5.unlinkSync(pidFile);
2418
+ } catch {
2419
+ }
2420
+ }
2421
+ } else {
2422
+ if (daemonPid) {
2423
+ try {
2424
+ const killed = await killProcessTree2(daemonPid);
2425
+ if (killed) {
2426
+ killedAny = true;
2427
+ try {
2428
+ if (fs5.existsSync(pidFile)) fs5.unlinkSync(pidFile);
2429
+ } catch {
2430
+ }
2431
+ }
2432
+ } catch {
2433
+ }
2434
+ }
2435
+ const portPid = await findPidByPort(port, process.pid);
2436
+ if (portPid) {
2437
+ const killedByPort = await killProcessOnPort(port);
2438
+ if (killedByPort) {
2439
+ killedAny = true;
2440
+ }
2441
+ }
2442
+ }
2443
+ let portReleased = false;
2444
+ for (let i = 0; i < 10; i++) {
2445
+ const remainingPid = await findPidByPort(port, process.pid);
2446
+ if (!remainingPid) {
2447
+ portReleased = true;
2448
+ break;
2449
+ }
2450
+ await new Promise((r) => setTimeout(r, 200));
2451
+ }
2452
+ if (!portReleased) {
2453
+ return false;
2454
+ }
2455
+ return killedAny;
2456
+ }
2457
+ var execAsync, WS_PORT2, DAEMON_DIR, PID_FILE;
2458
+ var init_daemon = __esm({
2459
+ "src/bridge/daemon.ts"() {
2460
+ "use strict";
2461
+ init_bridge_client();
2462
+ execAsync = promisify(exec);
2463
+ WS_PORT2 = 18999;
2464
+ DAEMON_DIR = path5.join(os4.homedir(), ".robot-wxt");
2465
+ PID_FILE = getDaemonPidFile(WS_PORT2);
2466
+ }
2467
+ });
2468
+
2469
+ // src/mcp/server.ts
2470
+ var server_exports = {};
2471
+ __export(server_exports, {
2472
+ startMcpServer: () => startMcpServer
2473
+ });
2474
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2475
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2476
+ async function startMcpServer(options = {}) {
2477
+ const mode = options.mode ?? "cdp";
2478
+ const agentMode = options.agentMode ?? "tools";
2479
+ const isAgentMode = agentMode === "agent";
2480
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
2481
+ if (mode === "cdp") {
2482
+ if (isAgentMode) {
2483
+ throw new Error("--agent \u5B50\u4EE3\u7406\u59D4\u6258\u6A21\u5F0F\u4EC5\u5728 WXT \u6A21\u5F0F\u53EF\u7528\uFF0C\u8BF7\u4F7F\u7528: webmcp-cli --mode wxt mcp --agent");
2484
+ }
2485
+ await startCdpMcpServer(server, isAgentMode);
2486
+ } else {
2487
+ await startWxtMcpServer(server, isAgentMode, options);
2488
+ }
2489
+ const transport = new StdioServerTransport();
2490
+ await server.connect(transport);
2491
+ process.stderr.write(
2492
+ `[webmcp-cli] MCP Server \u5DF2\u542F\u52A8\uFF08${SERVER_NAME} v${SERVER_VERSION}\uFF0C\u6A21\u5F0F: ${mode}${isAgentMode ? "/agent" : "/tools"}\uFF09
2493
+ `
2494
+ );
2495
+ }
2496
+ async function startCdpMcpServer(server, isAgentMode) {
2497
+ const { CdpBrowserAdapter: CdpBrowserAdapter2 } = await Promise.resolve().then(() => (init_cdp_adapter(), cdp_adapter_exports));
2498
+ const adapter = new CdpBrowserAdapter2();
2499
+ registerCommonTools(server, adapter, isAgentMode);
2500
+ process.on("SIGINT", async () => {
2501
+ await adapter.dispose();
2502
+ process.exit(0);
2503
+ });
2504
+ process.on("SIGTERM", async () => {
2505
+ await adapter.dispose();
2506
+ process.exit(0);
2507
+ });
2508
+ }
2509
+ async function startWxtMcpServer(server, isAgentMode, options) {
2510
+ const { WxtBrowserAdapter: WxtBrowserAdapter2 } = await Promise.resolve().then(() => (init_wxt_adapter(), wxt_adapter_exports));
2511
+ const { ensureBridgeDaemon: ensureBridgeDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
2512
+ const daemonResult = await ensureBridgeDaemon2({
2513
+ token: options.token,
2514
+ port: options.wsPort,
2515
+ silent: true
2516
+ });
2517
+ const adapter = new WxtBrowserAdapter2(options.token, options.wsPort);
2518
+ await adapter.ensureConnected();
2519
+ if (daemonResult.newlyStarted) {
2520
+ process.stderr.write("[webmcp-cli] \u{1F680} \u672C\u5730\u6865\u63A5\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u7B49\u5F85 Chrome \u6269\u5C55\u8FDE\u63A5...\n");
2521
+ }
2522
+ const client = adapter.bridgeClient;
2523
+ registerCommonTools(server, adapter, isAgentMode);
2524
+ registerWxtTools(server, adapter, isAgentMode, server);
2525
+ registerSkillsResources(server, adapter);
2526
+ client.on("connected", () => {
2527
+ process.stderr.write("[webmcp-cli] \u5DF2\u8FDE\u63A5\u5230 Chrome \u6269\u5C55\n");
2528
+ try {
2529
+ server.server.notification({ method: "notifications/resources/list_changed" });
2530
+ } catch {
2531
+ }
2532
+ });
2533
+ client.on("disconnected", () => {
2534
+ process.stderr.write("[webmcp-cli] \u4E0E Chrome \u6269\u5C55\u7684\u8FDE\u63A5\u5DF2\u65AD\u5F00\n");
2535
+ });
2536
+ process.on("SIGINT", async () => {
2537
+ await adapter.dispose();
2538
+ process.exit(0);
2539
+ });
2540
+ process.on("SIGTERM", async () => {
2541
+ await adapter.dispose();
2542
+ process.exit(0);
2543
+ });
2544
+ const tokenFingerprint = options.token ? `${options.token.slice(0, 6)}...` : "(auto)";
2545
+ process.stderr.write(`[webmcp-cli] \u{1F511} Token \u6307\u7EB9: ${tokenFingerprint} (\u5B8C\u6574\u51ED\u636E\u89C1 ~/.robot-wxt/bridge-token)
2546
+ `);
2547
+ }
2548
+ function registerCommonTools(server, adapter, isAgentMode) {
2549
+ server.registerTool(
2550
+ "browser_state",
2551
+ {
2552
+ description: "\u83B7\u53D6\u5F53\u524D\u6D4F\u89C8\u5668\u72B6\u6001\uFF0C\u5305\u62EC\uFF1A\u5F53\u524D tab \u7684 URL / \u6807\u9898\u3001\u6240\u6709\u5DF2\u6253\u5F00\u7684 tab \u5217\u8868\u3001\u5F53\u524D\u9875\u9762\u6CE8\u518C\u7684 WebMCP \u5DE5\u5177\u5217\u8868\u3002\u5728\u6267\u884C\u64CD\u4F5C\u524D\uFF0C\u5EFA\u8BAE\u5148\u8C03\u7528\u6B64\u5DE5\u5177\u4E86\u89E3\u5F53\u524D\u6D4F\u89C8\u5668\u4E0A\u4E0B\u6587\u3002",
2553
+ inputSchema: BrowserStateInput.shape
2554
+ },
2555
+ async (args) => {
2556
+ const result = await adapter.getState(args.tabId);
2557
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2558
+ }
2559
+ );
2560
+ if (!isAgentMode) {
2561
+ server.registerTool(
2562
+ "browser_tool_call",
2563
+ {
2564
+ description: "\u6267\u884C\u5F53\u524D\u9875\u9762\u6CE8\u518C\u7684 WebMCP \u5DE5\u5177\uFF08\u5982 click\u3001input\u3001scroll \u7B49\uFF09\u3002\u5DE5\u5177\u540D\u79F0\u548C\u53C2\u6570 schema \u901A\u8FC7 browser_state \u8FD4\u56DE\u7684 webmcpTools \u5B57\u6BB5\u83B7\u53D6\u3002",
2565
+ inputSchema: BrowserToolCallInput.shape
2566
+ },
2567
+ async (args) => {
2568
+ const result = await adapter.callTool(args.toolName, args.toolArgs ?? {}, args.tabId);
2569
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2570
+ }
2571
+ );
2572
+ server.registerTool(
2573
+ "browser_tabs_open",
2574
+ { description: "\u5728\u6D4F\u89C8\u5668\u4E2D\u6253\u5F00\u4E00\u4E2A\u65B0\u6807\u7B7E\u9875\u5E76\u5BFC\u822A\u5230\u6307\u5B9A URL\u3002", inputSchema: BrowserTabsOpenInput.shape },
2575
+ async (args) => {
2576
+ const result = await adapter.tabs("open", { url: args.url });
2577
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2578
+ }
2579
+ );
2580
+ server.registerTool(
2581
+ "browser_tabs_close",
2582
+ { description: "\u5173\u95ED\u6307\u5B9A\u6807\u7B7E\u9875\uFF08\u4E0D\u6307\u5B9A tabId \u65F6\u5173\u95ED\u5F53\u524D\u6D3B\u8DC3\u6807\u7B7E\u9875\uFF09\u3002", inputSchema: BrowserTabsCloseInput.shape },
2583
+ async (args) => {
2584
+ const result = await adapter.tabs("close", { tabId: args.tabId });
2585
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2586
+ }
2587
+ );
2588
+ server.registerTool(
2589
+ "browser_tabs_switch",
2590
+ {
2591
+ description: "\u5C06\u7126\u70B9\u5207\u6362\u5230\u6307\u5B9A\u6807\u7B7E\u9875\uFF08tabId \u901A\u8FC7 browser_state \u8FD4\u56DE\u7684 tabs \u5217\u8868\u83B7\u53D6\uFF09\u3002",
2592
+ inputSchema: BrowserTabsSwitchInput.shape
2593
+ },
2594
+ async (args) => {
2595
+ const result = await adapter.tabs("switch", { tabId: args.tabId, activateVisible: args.activateVisible });
2596
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2597
+ }
2598
+ );
2599
+ server.registerTool(
2600
+ "browser_tabs_back",
2601
+ { description: "\u5728\u6307\u5B9A\u6807\u7B7E\u9875\uFF08\u6216\u5F53\u524D\u6D3B\u8DC3\u6807\u7B7E\u9875\uFF09\u4E2D\u6267\u884C\u6D4F\u89C8\u5668\u540E\u9000\u64CD\u4F5C\u3002", inputSchema: BrowserTabsNavInput.shape },
2602
+ async (args) => {
2603
+ const result = await adapter.tabs("back", { tabId: args.tabId });
2604
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2605
+ }
2606
+ );
2607
+ server.registerTool(
2608
+ "browser_tabs_forward",
2609
+ { description: "\u5728\u6307\u5B9A\u6807\u7B7E\u9875\uFF08\u6216\u5F53\u524D\u6D3B\u8DC3\u6807\u7B7E\u9875\uFF09\u4E2D\u6267\u884C\u6D4F\u89C8\u5668\u524D\u8FDB\u64CD\u4F5C\u3002", inputSchema: BrowserTabsNavInput.shape },
2610
+ async (args) => {
2611
+ const result = await adapter.tabs("forward", { tabId: args.tabId });
2612
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2613
+ }
2614
+ );
2615
+ }
2616
+ }
2617
+ function registerWxtTools(server, adapter, isAgentMode, mcpServer) {
2618
+ server.registerTool(
2619
+ "browser_sub_agent_run",
2620
+ {
2621
+ description: isAgentMode ? "\u300ATiny Robot \u7AEF\u4FA7\u81EA\u4E3B\u5B50\u4EE3\u7406\u300B\u5C06\u590D\u6742\u6216\u957F\u6D41\u7A0B\u7684\u6D4F\u89C8\u5668\u64CD\u4F5C\u4EFB\u52A1\u59D4\u6258\u7ED9\u6269\u5C55\u7AEF\u4FA7\u7684 Tiny Robot \u5B50\u4EE3\u7406\u95ED\u73AF\u6267\u884C\u3002\u5B50\u4EE3\u7406\u5C06\u5728\u7AEF\u4FA7\u81EA\u4E3B\u89C2\u5BDF\u9875\u9762\u3001\u89C4\u5212\u6B65\u9AA4\u5E76\u6267\u884C\u539F\u5B50\u64CD\u4F5C\uFF0C\u6267\u884C\u5B8C\u6BD5\u540E\u8FD4\u56DE\u6700\u7EC8\u7ED3\u679C\uFF0C\u6781\u5927\u5730\u8282\u7701\u4E3B Agent \u4E0A\u4E0B\u6587 Token\u3002" : "\u300A\u9AD8\u5C42\u4EFB\u52A1\u59D4\u6258\u300B\u5C06\u590D\u6742\u3001\u957F\u6D41\u7A0B\u6216\u9700\u53CD\u590D\u8BD5\u9519\u7684\u6D4F\u89C8\u5668\u64CD\u4F5C\u4EFB\u52A1\u59D4\u6258\u7ED9 Chrome \u6269\u5C55\u5185\u90E8\u7684 Tiny Robot \u5B50\u4EE3\u7406\uFF08Sub-Agent\uFF09\u81EA\u4E3B\u95ED\u73AF\u6267\u884C\u3002\u5B50\u4EE3\u7406\u5C06\u5728\u7AEF\u4FA7\u81EA\u4E3B\u8FDB\u884C\u9875\u9762\u63A2\u7D22\u3001\u89C4\u5212\u548C\u539F\u5B50\u64CD\u4F5C\uFF0C\u6267\u884C\u5B8C\u6BD5\u540E\u8FD4\u56DE\u6700\u7EC8\u7ED3\u679C\uFF0C\u6781\u5927\u5730\u8282\u7701\u5916\u90E8\u4E3B Agent \u7684\u4E0A\u4E0B\u6587 Token\u3002",
2622
+ inputSchema: BrowserSubAgentRunInput.shape
2623
+ },
2624
+ async (args, extra) => {
2625
+ const progressToken = extra?._meta?.progressToken;
2626
+ const onProgress = progressToken != null ? (params) => {
2627
+ if (params?.progressToken && params.progressToken !== progressToken) return;
2628
+ try {
2629
+ mcpServer.server.notification({
2630
+ method: "notifications/progress",
2631
+ params: { progressToken, progress: params?.step ?? 0, total: params?.total ?? args.maxSteps ?? 8, message: params?.message ?? "" }
2632
+ });
2633
+ } catch {
2634
+ }
2635
+ } : void 0;
2636
+ if (onProgress) adapter.bridgeClient.on("progress", onProgress);
2637
+ try {
2638
+ const result = await adapter.runSubAgent(args.instruction, {
2639
+ tabId: args.tabId,
2640
+ maxSteps: args.maxSteps,
2641
+ progressToken
2642
+ });
2643
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2644
+ } finally {
2645
+ if (onProgress) adapter.bridgeClient.off("progress", onProgress);
2646
+ }
2647
+ }
2648
+ );
2649
+ if (!isAgentMode) {
2650
+ server.registerTool(
2651
+ "browser_skills_list",
2652
+ {
2653
+ description: "\u3010\u6280\u80FD\u6E10\u8FDB\u5F0F\u62AB\u9732 - L1 \u7D22\u5F15\u5C42\u3011\u83B7\u53D6\u5F53\u524D\u6D4F\u89C8\u5668\u6269\u5C55\u4E2D\u5DF2\u7EF4\u62A4\u7684\u6240\u6709 Skills \u6280\u80FD\u7B80\u77ED\u6E05\u5355\uFF08\u4EC5\u8FD4\u56DE\u540D\u79F0\u3001\u7B80\u4ECB\uFF0C\u6D88\u8017\u6781\u5C11 Token\uFF09\u3002\u5728\u81EA\u4E3B\u6267\u884C\u590D\u6742\u6216\u5782\u76F4\u9886\u57DF\u4EFB\u52A1\u524D\uFF0C\u53EF\u5148\u8C03\u7528\u6B64\u5DE5\u5177\u5339\u914D\u6700\u9002\u7528\u7684\u6280\u80FD\u3002",
2654
+ inputSchema: BrowserSkillsListInput.shape
2655
+ },
2656
+ async () => {
2657
+ const result = await adapter.listSkills();
2658
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2659
+ }
2660
+ );
2661
+ server.registerTool(
2662
+ "browser_skills_read",
2663
+ {
2664
+ description: "\u3010\u6280\u80FD\u6E10\u8FDB\u5F0F\u62AB\u9732 - L2 \u5951\u7EA6\u5C42\u3011\u6839\u636E\u6280\u80FD\u540D\u79F0\u6309\u9700\u8BFB\u53D6\u8BE5\u6280\u80FD\u7684\u5B8C\u6574\u8BE6\u7EC6\u6267\u884C\u6307\u4EE4 (instructions)\u3001\u5DE5\u4F5C\u6D41\u89C4\u8303\u4E0E\u8F93\u5165\u8F93\u51FA\u5951\u7EA6\u3002",
2665
+ inputSchema: BrowserSkillsReadInput.shape
2666
+ },
2667
+ async (args) => {
2668
+ const result = await adapter.getSkill(args.name);
2669
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2670
+ }
2671
+ );
2672
+ }
2673
+ }
2674
+ function registerSkillsResources(server, adapter) {
2675
+ server.registerResource(
2676
+ "skills_index",
2677
+ "skills://index",
2678
+ { description: "Skills L1\uFF1A\u6269\u5C55\u5DF2\u6CE8\u518C\u7684\u6240\u6709\u6280\u80FD\u5217\u8868\uFF08\u4EC5\u5143\u6570\u636E\uFF0C\u6D88\u8017\u6781\u5C11 Token\uFF09", mimeType: "application/json" },
2679
+ async () => {
2680
+ try {
2681
+ const skills = await adapter.listSkills();
2682
+ return { contents: [{ uri: "skills://index", text: JSON.stringify(skills, null, 2), mimeType: "application/json" }] };
2683
+ } catch (err) {
2684
+ return { contents: [{ uri: "skills://index", text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }), mimeType: "application/json" }] };
2685
+ }
2686
+ }
2687
+ );
2688
+ const skillDetailTemplate = new ResourceTemplate("skills://{skillName}/detail", { list: void 0 });
2689
+ server.registerResource(
2690
+ "skill_detail",
2691
+ skillDetailTemplate,
2692
+ { description: "Skills L2\uFF1A\u6309\u6280\u80FD\u540D\u5B9E\u65F6\u62C9\u53D6\u5B8C\u6574 SOP \u6267\u884C\u6307\u4EE4\u4E0E\u8F93\u5165\u8F93\u51FA\u5951\u7EA6", mimeType: "application/json" },
2693
+ async (uri, params) => {
2694
+ const { skillName } = params;
2695
+ try {
2696
+ const detail = await adapter.getSkill(skillName);
2697
+ return { contents: [{ uri: uri.href, text: JSON.stringify(detail, null, 2), mimeType: "application/json" }] };
2698
+ } catch (err) {
2699
+ return { contents: [{ uri: uri.href, text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }), mimeType: "application/json" }] };
2700
+ }
2701
+ }
2702
+ );
2703
+ const skillResourceTemplate = new ResourceTemplate("skills://{skillName}/{resourcePath}", { list: void 0 });
2704
+ server.registerResource(
2705
+ "skill_resource",
2706
+ skillResourceTemplate,
2707
+ { description: "Skills L3\uFF1A\u8BFB\u53D6\u6280\u80FD\u5173\u8054\u7684\u6DF1\u5165\u53C2\u8003\u8D44\u6599\u6216\u914D\u7F6E\u6587\u4EF6\uFF08\u6309\u9700\u52A0\u8F7D\uFF09", mimeType: "text/plain" },
2708
+ async (uri, params) => {
2709
+ const { skillName, resourcePath } = params;
2710
+ try {
2711
+ const text = await adapter.readSkillResource(skillName, resourcePath);
2712
+ return { contents: [{ uri: uri.href, text: String(text), mimeType: "text/plain" }] };
2713
+ } catch (err) {
2714
+ return { contents: [{ uri: uri.href, text: `\u8BFB\u53D6\u8D44\u6E90\u5931\u8D25: ${err instanceof Error ? err.message : String(err)}`, mimeType: "text/plain" }] };
2715
+ }
2716
+ }
2717
+ );
2718
+ }
2719
+ var SERVER_NAME, SERVER_VERSION;
2720
+ var init_server = __esm({
2721
+ "src/mcp/server.ts"() {
2722
+ "use strict";
2723
+ init_package();
2724
+ init_tools();
2725
+ SERVER_NAME = "webmcp-cli";
2726
+ SERVER_VERSION = package_default.version || "0.0.9";
2727
+ }
2728
+ });
2729
+
2730
+ // src/commands/agent.ts
2731
+ var agent_exports = {};
2732
+ __export(agent_exports, {
2733
+ handleAgentCommand: () => handleAgentCommand
2734
+ });
2735
+ async function handleAgentCommand(adapter, args) {
2736
+ const subCommand = args[0];
2737
+ const restArgs = args.slice(1);
2738
+ if (!subCommand || subCommand === "--help" || subCommand === "-h") {
2739
+ printAgentHelp();
2740
+ return;
2741
+ }
2742
+ if (subCommand === "run") {
2743
+ await handleAgentRun(adapter, restArgs);
2744
+ return;
2745
+ }
2746
+ console.error(`[\u9519\u8BEF] \u672A\u77E5\u5B50\u547D\u4EE4: agent ${subCommand}`);
2747
+ console.error("\u8FD0\u884C webmcp-cli --mode wxt agent --help \u67E5\u770B\u652F\u6301\u7684\u547D\u4EE4");
2748
+ process.exit(1);
2749
+ }
2750
+ async function handleAgentRun(adapter, args) {
2751
+ let instruction = "";
2752
+ let maxSteps;
2753
+ let tabId;
2754
+ const isJson = args.includes("--json");
2755
+ for (let i = 0; i < args.length; i++) {
2756
+ const arg = args[i];
2757
+ if (arg === "--max-steps" && args[i + 1]) {
2758
+ maxSteps = parseInt(args[++i], 10);
2759
+ } else if (arg === "--tab" && args[i + 1]) {
2760
+ tabId = parseInt(args[++i], 10);
2761
+ } else if (!arg.startsWith("-")) {
2762
+ instruction = arg;
2763
+ }
2764
+ }
2765
+ if (!instruction) {
2766
+ console.error('[\u9519\u8BEF] \u8BF7\u63D0\u4F9B\u4EFB\u52A1\u6307\u4EE4\u3002\u793A\u4F8B\uFF1Awebmcp-cli --mode wxt agent run "\u5E2E\u6211\u641C\u7D22\u6700\u65B0\u65B0\u95FB"');
2767
+ process.exit(1);
2768
+ }
2769
+ if (!isJson) {
2770
+ console.log(`\u{1F916} \u6B63\u5728\u59D4\u6258 Tiny Robot \u5B50\u4EE3\u7406\u6267\u884C\u4EFB\u52A1\uFF1A\u300C${instruction}\u300D`);
2771
+ if (maxSteps) console.log(` \u6700\u5927\u6B65\u6570\uFF1A${maxSteps}`);
2772
+ }
2773
+ try {
2774
+ const result = await adapter.runSubAgent(instruction, { tabId, maxSteps });
2775
+ if (isJson) {
2776
+ console.log(JSON.stringify(result, null, 2));
2777
+ } else {
2778
+ console.log("\n\u2705 \u5B50\u4EE3\u7406\u6267\u884C\u5B8C\u6210\uFF1A");
2779
+ console.log(JSON.stringify(result, null, 2));
2780
+ }
2781
+ } catch (err) {
2782
+ const msg = err instanceof Error ? err.message : String(err);
2783
+ if (isJson) {
2784
+ console.log(JSON.stringify({ success: false, error: msg }));
2785
+ } else {
2786
+ console.error(`\u274C \u6267\u884C\u5931\u8D25\uFF1A${msg}`);
2787
+ }
2788
+ process.exit(1);
2789
+ }
2790
+ }
2791
+ function printAgentHelp() {
2792
+ console.log(`
2793
+ webmcp-cli --mode wxt agent - \u59D4\u6258 Chrome \u6269\u5C55\u5185 Tiny Robot \u5B50\u4EE3\u7406\u6267\u884C\u9AD8\u5C42\u4EFB\u52A1
2794
+
2795
+ \u7528\u6CD5\uFF1A
2796
+ webmcp-cli --mode wxt agent run "<\u6307\u4EE4>" \u59D4\u6258\u5B50\u4EE3\u7406\u81EA\u4E3B\u6267\u884C\u81EA\u7136\u8BED\u8A00\u4EFB\u52A1
2797
+
2798
+ \u9009\u9879\uFF1A
2799
+ --max-steps <n> \u6700\u5927\u6267\u884C\u6B65\u6570\uFF08\u9ED8\u8BA4 8\uFF0C\u6700\u5927 20\uFF09
2800
+ --tab <tabId> \u6307\u5B9A\u76EE\u6807\u6807\u7B7E\u9875 ID
2801
+ --json \u8F93\u51FA JSON \u683C\u5F0F\u7ED3\u679C
2802
+
2803
+ \u793A\u4F8B\uFF1A
2804
+ webmcp-cli --mode wxt agent run "\u5E2E\u6211\u628A\u5F53\u524D\u5546\u54C1\u52A0\u5165\u8D2D\u7269\u8F66"
2805
+ webmcp-cli --mode wxt agent run "\u641C\u7D22\u5E76\u6536\u85CF\u524D3\u7BC7\u6587\u7AE0" --max-steps 10
2806
+ `.trim());
2807
+ }
2808
+ var init_agent = __esm({
2809
+ "src/commands/agent.ts"() {
2810
+ "use strict";
2811
+ }
2812
+ });
2813
+
2814
+ // src/commands/skills.ts
2815
+ var skills_exports = {};
2816
+ __export(skills_exports, {
2817
+ handleSkillsCommand: () => handleSkillsCommand
2818
+ });
2819
+ async function handleSkillsCommand(adapter, args) {
2820
+ const subCommand = args[0];
2821
+ const restArgs = args.slice(1);
2822
+ const isJson = args.includes("--json");
2823
+ if (!subCommand || subCommand === "--help" || subCommand === "-h") {
2824
+ printSkillsHelp();
2825
+ return;
2826
+ }
2827
+ if (subCommand === "list") {
2828
+ await handleSkillsList(adapter, isJson);
2829
+ return;
2830
+ }
2831
+ if (subCommand === "get") {
2832
+ const skillName = restArgs.find((a) => !a.startsWith("-"));
2833
+ if (!skillName) {
2834
+ console.error("[\u9519\u8BEF] \u8BF7\u63D0\u4F9B\u6280\u80FD\u540D\u79F0\u3002\u793A\u4F8B\uFF1Awebmcp-cli --mode wxt skills get <skillName>");
2835
+ process.exit(1);
2836
+ }
2837
+ await handleSkillsGet(adapter, skillName, isJson);
2838
+ return;
2839
+ }
2840
+ console.error(`[\u9519\u8BEF] \u672A\u77E5\u5B50\u547D\u4EE4: skills ${subCommand}`);
2841
+ printSkillsHelp();
2842
+ process.exit(1);
2843
+ }
2844
+ async function handleSkillsList(adapter, isJson) {
2845
+ try {
2846
+ const result = await adapter.listSkills();
2847
+ if (isJson) {
2848
+ console.log(JSON.stringify(result, null, 2));
2849
+ } else {
2850
+ const skills = result;
2851
+ if (!skills || skills.length === 0) {
2852
+ console.log("\uFF08\u5F53\u524D\u6269\u5C55\u672A\u6CE8\u518C\u4EFB\u4F55\u6280\u80FD\uFF09");
2853
+ return;
2854
+ }
2855
+ console.log(`
2856
+ \u{1F4DA} \u5DF2\u6CE8\u518C Skills\uFF08\u5171 ${skills.length} \u4E2A\uFF09\uFF1A
2857
+ `);
2858
+ for (const skill of skills) {
2859
+ console.log(` \u2022 ${skill.name}`);
2860
+ if (skill.description) {
2861
+ console.log(` ${skill.description.slice(0, 100)}${skill.description.length > 100 ? "..." : ""}`);
2862
+ }
2863
+ }
2864
+ console.log(`
2865
+ \u63D0\u793A\uFF1A\u4F7F\u7528 "webmcp-cli --mode wxt skills get <name>" \u67E5\u770B\u6280\u80FD\u8BE6\u60C5`);
2866
+ }
2867
+ } catch (err) {
2868
+ const msg = err instanceof Error ? err.message : String(err);
2869
+ if (isJson) {
2870
+ console.log(JSON.stringify({ error: msg }));
2871
+ } else {
2872
+ console.error(`\u274C \u83B7\u53D6\u6280\u80FD\u5217\u8868\u5931\u8D25\uFF1A${msg}`);
2873
+ }
2874
+ process.exit(1);
2875
+ }
2876
+ }
2877
+ async function handleSkillsGet(adapter, name, isJson) {
2878
+ try {
2879
+ const result = await adapter.getSkill(name);
2880
+ if (isJson) {
2881
+ console.log(JSON.stringify(result, null, 2));
2882
+ } else {
2883
+ const skill = result;
2884
+ console.log(`
2885
+ \u{1F4D6} \u6280\u80FD\uFF1A${skill.name}`);
2886
+ if (skill.description) console.log(`\u63CF\u8FF0\uFF1A${skill.description}`);
2887
+ if (skill.instructions) {
2888
+ console.log("\n--- \u6307\u4EE4 ---");
2889
+ console.log(skill.instructions);
2890
+ }
2891
+ }
2892
+ } catch (err) {
2893
+ const msg = err instanceof Error ? err.message : String(err);
2894
+ if (isJson) {
2895
+ console.log(JSON.stringify({ error: msg }));
2896
+ } else {
2897
+ console.error(`\u274C \u83B7\u53D6\u6280\u80FD\u8BE6\u60C5\u5931\u8D25\uFF1A${msg}`);
2898
+ }
2899
+ process.exit(1);
2900
+ }
2901
+ }
2902
+ function printSkillsHelp() {
2903
+ console.log(`
2904
+ webmcp-cli --mode wxt skills - \u67E5\u8BE2\u4E0E\u8BFB\u53D6\u6D4F\u89C8\u5668\u6269\u5C55\u4E2D\u6CE8\u518C\u7684 Skills \u6280\u80FD
2905
+
2906
+ \u7528\u6CD5\uFF1A
2907
+ webmcp-cli --mode wxt skills list \u5217\u51FA\u6240\u6709\u5DF2\u6CE8\u518C\u7684\u6280\u80FD
2908
+ webmcp-cli --mode wxt skills get <name> \u8BFB\u53D6\u6307\u5B9A\u6280\u80FD\u7684\u8BE6\u7EC6\u6267\u884C\u6307\u4EE4
2909
+
2910
+ \u9009\u9879\uFF1A
2911
+ --json \u8F93\u51FA JSON \u683C\u5F0F\u7ED3\u679C
2912
+
2913
+ \u793A\u4F8B\uFF1A
2914
+ webmcp-cli --mode wxt skills list
2915
+ webmcp-cli --mode wxt skills get order-management
2916
+ `.trim());
2917
+ }
2918
+ var init_skills = __esm({
2919
+ "src/commands/skills.ts"() {
2920
+ "use strict";
2921
+ }
2922
+ });
2923
+
2924
+ // src/bin.ts
2925
+ import fs6 from "fs";
2926
+ import path6 from "path";
2927
+ import os5 from "os";
2928
+ import { Command } from "commander";
2929
+ import pc2 from "picocolors";
2930
+ import { readFileSync as readFileSync2 } from "fs";
2931
+ import { resolve as resolve2 } from "path";
2932
+
2933
+ // src/expand-file-refs.ts
2934
+ import { readFileSync } from "fs";
2935
+ import { dirname, isAbsolute, normalize, resolve } from "path";
2936
+ import { fileURLToPath } from "url";
2937
+ var JSON_STRING_LITERAL = /"(?:[^"\\]|\\.)*"/g;
2938
+ var UNQUOTED_FILE_REF = /(?<![\\"])@(base64)?file:([^\s"',}\]]+)/g;
2939
+ var FILE_REF_PREFIX = /^@(base64)?file:(.+)$/s;
2940
+ function resolveFilePath(filePath, baseDir = process.cwd()) {
2941
+ let normalizedPath = filePath.trim();
2942
+ if (normalizedPath.startsWith('"') && normalizedPath.endsWith('"') || normalizedPath.startsWith("'") && normalizedPath.endsWith("'")) {
2943
+ normalizedPath = normalizedPath.slice(1, -1).trim();
2944
+ }
2945
+ if (normalizedPath.startsWith("file://")) {
2946
+ normalizedPath = fileURLToPath(normalizedPath);
2947
+ }
2948
+ if (isAbsolute(normalizedPath)) {
2949
+ return normalize(normalizedPath);
2950
+ }
2951
+ return resolve(baseDir, normalizedPath);
2952
+ }
2953
+ function decodeFilePath(rawPath) {
2954
+ return rawPath.replace(/\\\\/g, "\\");
2955
+ }
2956
+ function readFileRefContent(filePath, baseDir, base64) {
2957
+ const absPath = resolveFilePath(filePath, baseDir);
2958
+ let content;
2959
+ try {
2960
+ content = readFileSync(absPath, "utf-8");
2961
+ } catch (e) {
2962
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}" (${absPath}): ${e.message}`);
2963
+ }
2964
+ return base64 ? Buffer.from(content, "utf-8").toString("base64") : content;
2965
+ }
2966
+ function expandFileRefValue(rawInner, baseDir) {
2967
+ const match = rawInner.match(FILE_REF_PREFIX);
2968
+ if (!match) {
2969
+ return null;
2970
+ }
2971
+ const [, base64Flag, rawPath] = match;
2972
+ const filePath = decodeFilePath(rawPath);
2973
+ const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
2974
+ return JSON.stringify(value);
2975
+ }
2976
+ function expandQuotedFileRefs(argsJson, baseDir) {
2977
+ return argsJson.replace(JSON_STRING_LITERAL, (jsonStringLiteral) => {
2978
+ const rawInner = jsonStringLiteral.slice(1, -1);
2979
+ const replacement = expandFileRefValue(rawInner, baseDir);
2980
+ return replacement ?? jsonStringLiteral;
2981
+ });
2982
+ }
2983
+ function expandUnquotedFileRefs(argsJson, baseDir) {
2984
+ return argsJson.replace(UNQUOTED_FILE_REF, (match, base64Flag, filePath) => {
2985
+ const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
2986
+ return JSON.stringify(value);
2987
+ });
2988
+ }
2989
+ function expandFileRefs(argsJson, baseDir = process.cwd()) {
2990
+ const withQuotedExpanded = expandQuotedFileRefs(argsJson, baseDir);
2991
+ return expandUnquotedFileRefs(withQuotedExpanded, baseDir);
2992
+ }
2993
+ function getFileBaseDir(fileOption) {
2994
+ if (!fileOption) {
2995
+ return process.cwd();
2996
+ }
2997
+ return dirname(resolve(process.cwd(), fileOption));
2998
+ }
2999
+
3000
+ // src/parse-run-args.ts
3001
+ import JSON5 from "json5";
3002
+ function cleanJsonString(str) {
3003
+ let cleaned = str.trim();
3004
+ if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
3005
+ cleaned = cleaned.slice(1, -1).trim();
3006
+ }
3007
+ return cleaned;
3008
+ }
3009
+ function looksLikeObjectBody(raw) {
3010
+ return /^("?[\w-]+"?\s*:)/.test(raw);
3011
+ }
3012
+ function joinShellArgs(args) {
3013
+ const cleaned = args.map(cleanJsonString);
3014
+ const first = cleaned[0] ?? "";
3015
+ if (first.startsWith("{")) {
3016
+ return cleaned.join(" ");
3017
+ }
3018
+ if (looksLikeObjectBody(first)) {
3019
+ let joined = cleaned.join(" ");
3020
+ if (!joined.startsWith("{")) {
3021
+ joined = `{${joined}}`;
3022
+ }
3023
+ if (!joined.endsWith("}")) {
3024
+ joined = `${joined}}`;
526
3025
  }
3026
+ return joined;
527
3027
  }
528
- if (!targetPage) {
529
- throw new Error("\u65E0\u6CD5\u83B7\u53D6\u76EE\u6807\u9875\u9762");
530
- }
531
- await injectWebMCPPolyfillAndTools(targetPage);
532
- return targetPage;
3028
+ return cleaned.join(" ");
533
3029
  }
534
- async function injectIntoPage(page) {
535
- await injectWebMCPPolyfillAndTools(page, true);
3030
+ function insertMissingCommas(raw) {
3031
+ return raw.replace(/"(\s+)("?[\w-]+"?\s*:)/g, '",$2');
536
3032
  }
537
- async function injectWebMCPPolyfillAndTools(page, force = false) {
538
- const polyfillReady = !force && await page.evaluate(() => {
539
- return !!window.__webmcpcli_init;
540
- }).catch(() => false);
541
- if (!polyfillReady) {
542
- console.log(pc.cyan("\u5F53\u524D\u9875\u9762\u5C1A\u672A\u6CE8\u5165 WebMCP \u73AF\u5883\uFF0C\u6B63\u5728\u6267\u884C\u81EA\u52A8\u6CE8\u5165..."));
543
- const injectScriptPath = path.resolve(__dirname, "inject-bundle.js");
544
- if (!fs.existsSync(injectScriptPath)) {
545
- throw new Error(`Cannot find inject-bundle.js at ${injectScriptPath}. Please ensure you run 'pnpm build:inject' first.`);
3033
+ function quoteUnquotedStringValues(raw) {
3034
+ return raw.replace(
3035
+ /([{,]\s*(?:"([^"]+)"|([\w-]+))\s*:\s*)(?!"@(?:base64)?file:|"|true|false|null|-?\d|\{|\[)([^",}\]]+?)(?=\s*[,}])/g,
3036
+ (_match, prefix, _quotedKey, _unquotedKey, value) => {
3037
+ const trimmed = value.trim();
3038
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed)) {
3039
+ return prefix + trimmed;
3040
+ }
3041
+ return prefix + JSON.stringify(trimmed);
546
3042
  }
547
- const scriptContent = fs.readFileSync(injectScriptPath, "utf-8");
3043
+ );
3044
+ }
3045
+ function normalizeShellDamagedJson(raw) {
3046
+ let normalized = raw.trim();
3047
+ normalized = insertMissingCommas(normalized);
3048
+ normalized = quoteUnquotedStringValues(normalized);
3049
+ return normalized;
3050
+ }
3051
+ function buildArgsFromKeyValuePairs(args) {
3052
+ const obj = {};
3053
+ for (const pair of args) {
3054
+ const index = pair.indexOf("=");
3055
+ if (index === -1) {
3056
+ throw new Error(`\u65E0\u6548\u7684\u53C2\u6570\u683C\u5F0F "${pair}"\u3002\u8BF7\u4F7F\u7528 key=value\uFF0C\u6216 JSON / -f \u6587\u4EF6\u3002`);
3057
+ }
3058
+ const key = pair.substring(0, index).trim();
3059
+ let val = cleanJsonString(pair.substring(index + 1).trim());
548
3060
  try {
549
- await page.evaluate(scriptContent);
550
- } catch (err) {
551
- const msg = err instanceof Error ? err.message : String(err);
552
- throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
3061
+ obj[key] = JSON.parse(val);
3062
+ } catch {
3063
+ obj[key] = val;
553
3064
  }
554
- await page.waitForFunction(() => {
555
- const mcp = navigator.modelContext;
556
- return mcp && typeof mcp.registerTool === "function";
557
- }, { timeout: 1e4 }).catch(() => {
558
- console.warn("\u7B49\u5F85 WebMCP registerTool \u63A5\u53E3\u5C31\u7EEA\u8D85\u65F6\uFF0C\u6B63\u5728\u7EE7\u7EED\u6CE8\u5165...");
559
- });
560
3065
  }
561
- await injectDomainTools(page);
3066
+ return JSON.stringify(obj);
562
3067
  }
563
- function getToolsBundleName(hostname) {
564
- const normalized = hostname.split(":")[0].toLowerCase();
565
- const toolsDir = path.resolve(__dirname, "webmcp-tools");
566
- if (!fs.existsSync(toolsDir)) {
567
- return null;
568
- }
569
- let files = [];
3068
+ function parseArgsJson(raw) {
3069
+ const trimmed = cleanJsonString(raw);
570
3070
  try {
571
- files = fs.readdirSync(toolsDir);
572
- } catch {
573
- return null;
574
- }
575
- const supportedDomains = files.filter((f) => f.endsWith(".js")).map((f) => f.slice(0, -3));
576
- if (supportedDomains.includes(normalized)) {
577
- return `webmcp-tools/${normalized}.js`;
578
- }
579
- for (const domain of supportedDomains) {
580
- if (normalized.endsWith("." + domain)) {
581
- return `webmcp-tools/${domain}.js`;
3071
+ return JSON.stringify(JSON5.parse(trimmed));
3072
+ } catch (firstError) {
3073
+ const repaired = normalizeShellDamagedJson(trimmed);
3074
+ try {
3075
+ return JSON.stringify(JSON5.parse(repaired));
3076
+ } catch {
3077
+ const hint = `\u63D0\u793A\uFF1Abash \u4E2D JSON \u5FC5\u987B\u7528\u5355\u5F15\u53F7\u5305\u88F9\uFF0C\u4F8B\u5982 '{"title":"\u6807\u9898","content":"@base64file:./article.md"}'\uFF1B\u6216\u4F7F\u7528 key=value\uFF1Atitle=\u6807\u9898 content=@base64file:./article.md\uFF1B\u6216\u4F7F\u7528 -f args.json\u3002`;
3078
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
3079
+ throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON \u6216 JS \u5BF9\u8C61: ${message}
3080
+ ${hint}`);
582
3081
  }
583
3082
  }
584
- return null;
585
3083
  }
586
- async function injectDomainTools(page) {
587
- let hostname;
588
- try {
589
- const url = new URL(page.url());
590
- hostname = url.hostname;
591
- } catch {
592
- return;
593
- }
594
- const bundleName = getToolsBundleName(hostname);
595
- if (!bundleName) {
596
- return;
3084
+ function prepareRunArgsJson(args, fileContent, baseDir) {
3085
+ if (fileContent !== void 0) {
3086
+ const trimmed = fileContent.trim();
3087
+ if (!trimmed) {
3088
+ throw new Error("\u901A\u8FC7 -f/--file \u6307\u5B9A\u7684\u53C2\u6570\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
3089
+ }
3090
+ const expanded2 = expandFileRefs(trimmed, baseDir);
3091
+ return parseArgsJson(expanded2);
597
3092
  }
598
- const toolBundlePath = path.resolve(__dirname, bundleName);
599
- if (!fs.existsSync(toolBundlePath)) {
600
- return;
3093
+ if (args.length === 0) {
3094
+ return "{}";
601
3095
  }
602
- const bundleKey = bundleName.replace(/^webmcp-tools\//, "").replace(/\.js$/, "");
603
- const flagKey = `__webmcptools_${bundleKey.replace(/[^a-zA-Z0-9]/g, "")}`;
604
- const alreadyInjected = await page.evaluate((key) => !!window[key], flagKey).catch(() => false);
605
- if (alreadyInjected) {
606
- return;
3096
+ const joined = joinShellArgs(args);
3097
+ let raw = "";
3098
+ if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
3099
+ raw = joined;
3100
+ } else if (args.every((arg) => arg.includes("="))) {
3101
+ raw = buildArgsFromKeyValuePairs(args);
3102
+ } else {
3103
+ raw = joined;
607
3104
  }
608
- console.log(pc.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
609
- const toolScript = fs.readFileSync(toolBundlePath, "utf-8");
610
- try {
611
- await page.evaluate(toolScript);
612
- console.log(pc.green(`\u5DF2\u4E3A ${hostname} \u6CE8\u5165\u9884\u7F6E\u5DE5\u5177`));
613
- } catch (err) {
614
- const msg = err instanceof Error ? err.message : String(err);
615
- console.warn(pc.yellow(`\u57DF\u540D\u5DE5\u5177\u6CE8\u5165\u5931\u8D25 (${hostname}): ${msg}`));
3105
+ if (!raw.trim()) {
3106
+ throw new Error("\u672A\u63D0\u4F9B\u6709\u6548\u53C2\u6570");
616
3107
  }
3108
+ const expanded = expandFileRefs(raw, baseDir);
3109
+ return parseArgsJson(expanded);
617
3110
  }
618
3111
 
619
3112
  // src/commands/state.ts
3113
+ init_browser();
620
3114
  async function stateCommand({ tabid }) {
621
3115
  const browser = await connectBrowser();
622
3116
  try {
@@ -625,11 +3119,19 @@ async function stateCommand({ tabid }) {
625
3119
  const state = await page.evaluate(async () => {
626
3120
  const url = document.URL;
627
3121
  const title = document.title;
628
- const mcp = document.modelContext;
3122
+ const mcp = document.modelContext || navigator.modelContext;
629
3123
  let webmcpTools = [];
630
- if (mcp && typeof mcp.listTools === "function") {
631
- const toolsResult = await mcp.getTools();
632
- webmcpTools = toolsResult?.tools || toolsResult || [];
3124
+ if (mcp && typeof mcp.getTools === "function") {
3125
+ try {
3126
+ const toolsResult = await mcp.getTools();
3127
+ const list = toolsResult?.tools || toolsResult || [];
3128
+ webmcpTools = list.filter((t) => t && typeof t.name === "string").map((t) => ({
3129
+ name: t.name,
3130
+ description: typeof t.description === "string" ? t.description.slice(0, 200) : void 0
3131
+ }));
3132
+ } catch {
3133
+ webmcpTools = [];
3134
+ }
633
3135
  }
634
3136
  return { url, title, webmcpTools };
635
3137
  });
@@ -661,156 +3163,8 @@ async function stateCommand({ tabid }) {
661
3163
  }
662
3164
  }
663
3165
 
664
- // src/zhihu/create-article.ts
665
- import os2 from "os";
666
-
667
- // src/zhihu/markdown.ts
668
- import { Marked } from "marked";
669
- function stripLeadingH1(markdown) {
670
- return markdown.replace(/^#\s+[^\n]*\n+/, "");
671
- }
672
- function markdownToHtml(markdown) {
673
- const marked = new Marked({
674
- gfm: true,
675
- breaks: false,
676
- renderer: {
677
- code({ text, lang }) {
678
- const langClass = lang ? ` class="language-${lang}"` : "";
679
- const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
680
- return `<pre><code${langClass}>${escaped}</code></pre>`;
681
- },
682
- codespan({ text }) {
683
- const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
684
- return `<code>${escaped}</code>`;
685
- }
686
- }
687
- });
688
- return marked.parse(markdown);
689
- }
690
- function decodeBase64Content(content) {
691
- return Buffer.from(content, "base64").toString("utf-8");
692
- }
693
- var ZHIHU_WRITE_PAGE_RE = /^https:\/\/zhuanlan\.zhihu\.com\/(write|p\/\d+\/edit)/;
694
- function isZhihuWriteUrl(url) {
695
- return ZHIHU_WRITE_PAGE_RE.test(url);
696
- }
697
-
698
- // src/zhihu/create-article.ts
699
- function delay(ms) {
700
- return new Promise((resolve3) => setTimeout(resolve3, ms));
701
- }
702
- function getPasteModifier() {
703
- return os2.platform() === "darwin" ? "Meta" : "Control";
704
- }
705
- async function fillTitle(page, title) {
706
- await page.evaluate((titleText) => {
707
- const input = document.querySelector('textarea[placeholder*="\u6807\u9898"]') || document.querySelector(".WriteIndex-titleInput textarea") || document.querySelector(".WriteIndex-titleInput input");
708
- if (!input) throw new Error("\u672A\u627E\u5230\u6807\u9898\u8F93\u5165\u6846\uFF0C\u8BF7\u786E\u8BA4\u7F16\u8F91\u5668\u9875\u9762\u5DF2\u5B8C\u5168\u52A0\u8F7D");
709
- const setter = Object.getOwnPropertyDescriptor(
710
- input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
711
- "value"
712
- )?.set;
713
- if (setter) {
714
- setter.call(input, titleText);
715
- } else {
716
- input.value = titleText;
717
- }
718
- input.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, data: titleText }));
719
- input.dispatchEvent(new Event("change", { bubbles: true }));
720
- if (input.value.trim() !== titleText.trim()) {
721
- throw new Error("\u6807\u9898\u586B\u5199\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5");
722
- }
723
- }, title);
724
- }
725
- async function pasteHtmlToEditor(page, html) {
726
- const browser = page.browser();
727
- const tempPage = await browser.newPage();
728
- const modifier = getPasteModifier();
729
- try {
730
- const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
731
- await tempPage.setContent(fullHtml, { waitUntil: "domcontentloaded" });
732
- await delay(300);
733
- await tempPage.click("body");
734
- await tempPage.keyboard.down(modifier);
735
- await tempPage.keyboard.press("KeyA");
736
- await tempPage.keyboard.up(modifier);
737
- await delay(100);
738
- await tempPage.keyboard.down(modifier);
739
- await tempPage.keyboard.press("KeyC");
740
- await tempPage.keyboard.up(modifier);
741
- await delay(300);
742
- } finally {
743
- await tempPage.close();
744
- }
745
- await page.bringToFront();
746
- await page.waitForSelector(".public-DraftEditor-content", { timeout: 1e4 });
747
- await page.click(".public-DraftEditor-content");
748
- await delay(200);
749
- await page.keyboard.down(modifier);
750
- await page.keyboard.press("KeyA");
751
- await page.keyboard.up(modifier);
752
- await delay(100);
753
- await page.keyboard.press("Backspace");
754
- await delay(200);
755
- await page.keyboard.down(modifier);
756
- await page.keyboard.press("KeyV");
757
- await page.keyboard.up(modifier);
758
- await delay(1500);
759
- const probe = html.replace(/<[^>]+>/g, "").trim().slice(0, 15);
760
- const verifyResult = await page.evaluate((probeText) => {
761
- const editor = document.querySelector(".public-DraftEditor-content");
762
- if (!editor) return { ok: false, reason: "no editor" };
763
- const text = (editor.innerText || "").trim();
764
- if (!text) return { ok: false, reason: "empty" };
765
- const inner = editor.innerHTML;
766
- const hasRichFormat = inner.includes("data-text") || /font-weight:\s*bold|font-style:\s*italic|code-block|blockquote/i.test(inner);
767
- const normalizedText = text.replace(/\s+/g, "");
768
- const normalizedProbe = probeText.replace(/\s+/g, "");
769
- const hasProbe = normalizedText.includes(normalizedProbe);
770
- return {
771
- ok: text.length > 20 && hasProbe && (hasRichFormat || text.length > 50),
772
- reason: !hasProbe ? "probe mismatch" : !hasRichFormat && text.length <= 50 ? "no rich format" : void 0
773
- };
774
- }, probe);
775
- if (!verifyResult.ok) {
776
- throw new Error(
777
- `\u6B63\u6587\u586B\u5199\u5931\u8D25\uFF08Markdown \u8F6C HTML \u7C98\u8D34\u672A\u751F\u6548\uFF09${verifyResult.reason ? `: ${verifyResult.reason}` : ""}`
778
- );
779
- }
780
- }
781
- async function zhihuCreateArticle(page, args) {
782
- const { title, content } = args;
783
- if (!title?.trim()) throw new Error("\u53C2\u6570 title \u4E0D\u80FD\u4E3A\u7A7A");
784
- if (!content?.trim()) throw new Error("\u53C2\u6570 content \u4E0D\u80FD\u4E3A\u7A7A");
785
- const url = page.url();
786
- if (!isZhihuWriteUrl(url)) {
787
- throw new Error("\u5F53\u524D\u9875\u9762\u4E0D\u662F\u77E5\u4E4E\u4E13\u680F\u7F16\u8F91\u5668\uFF0C\u8BF7\u5148\u6253\u5F00 https://zhuanlan.zhihu.com/write");
788
- }
789
- let markdown;
790
- try {
791
- markdown = stripLeadingH1(decodeBase64Content(content));
792
- } catch {
793
- throw new Error("content \u4E0D\u662F\u6709\u6548\u7684 Base64 \u7F16\u7801\uFF0C\u8BF7\u68C0\u67E5\u53C2\u6570\u6216\u4F7F\u7528 @base64file: \u5F15\u7528\u6587\u4EF6");
794
- }
795
- if (!markdown.trim()) {
796
- throw new Error("\u6B63\u6587\u5185\u5BB9\u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 Markdown \u6587\u4EF6");
797
- }
798
- const html = markdownToHtml(markdown);
799
- await fillTitle(page, title.trim());
800
- await delay(500);
801
- await pasteHtmlToEditor(page, html);
802
- const result = {
803
- success: true,
804
- message: "\u6587\u7AE0\u6807\u9898\u548C\u6B63\u6587\u5DF2\u6210\u529F\u586B\u5199\u5230\u77E5\u4E4E\u4E13\u680F\u7F16\u8F91\u5668\uFF08Markdown \u5DF2\u8F6C\u4E3A\u5BCC\u6587\u672C\uFF09\uFF0C\u8349\u7A3F\u5C06\u81EA\u52A8\u4FDD\u5B58",
805
- title: title.trim(),
806
- contentLength: markdown.length
807
- };
808
- return {
809
- content: [{ type: "text", text: JSON.stringify(result) }]
810
- };
811
- }
812
-
813
3166
  // src/commands/run.ts
3167
+ init_browser();
814
3168
  async function runCommand({
815
3169
  toolName,
816
3170
  argsJson,
@@ -820,18 +3174,8 @@ async function runCommand({
820
3174
  try {
821
3175
  const page = await getTargetPage(browser, tabid);
822
3176
  const urlBefore = page.url();
3177
+ await injectIntoPage(page);
823
3178
  const cleanedArgs = argsJson.trim();
824
- if (toolName === "create_article") {
825
- let parsedArgs;
826
- try {
827
- parsedArgs = JSON.parse(cleanedArgs);
828
- } catch {
829
- throw new Error("\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON");
830
- }
831
- if (isZhihuWriteUrl(page.url())) {
832
- return await zhihuCreateArticle(page, parsedArgs);
833
- }
834
- }
835
3179
  let result;
836
3180
  try {
837
3181
  result = await page.evaluate(async (name, inputString) => {
@@ -895,6 +3239,7 @@ async function setClipboard(content) {
895
3239
  }
896
3240
 
897
3241
  // src/commands/tabs.ts
3242
+ init_browser();
898
3243
  function normalizeUrl(url) {
899
3244
  if (!url.startsWith("http://") && !url.startsWith("https://")) {
900
3245
  return "https://" + url;
@@ -1016,90 +3361,77 @@ async function tabsForwardCommand(tabid) {
1016
3361
  }
1017
3362
  }
1018
3363
 
1019
- // package.json
1020
- var package_default = {
1021
- name: "@opentiny/webmcp-cli",
1022
- version: "0.0.6",
1023
- type: "module",
1024
- description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
1025
- main: "dist/index.js",
1026
- types: "dist/index.d.ts",
1027
- bin: {
1028
- "webmcp-cli": "./dist/bin.js"
1029
- },
1030
- files: [
1031
- "dist",
1032
- "README.md"
1033
- ],
1034
- scripts: {
1035
- dev: "tsup --watch",
1036
- "build:inject": "node scripts/build-inject.mjs",
1037
- build: "tsup && pnpm run build:inject",
1038
- prepublishOnly: "pnpm run build",
1039
- "link:global": "npm install -g .",
1040
- start: "node dist/bin.js"
1041
- },
1042
- keywords: [
1043
- "chrome",
1044
- "puppeteer",
1045
- "cli"
1046
- ],
1047
- license: "MIT",
1048
- author: "OpenTiny Team",
1049
- engines: {
1050
- node: ">=16"
1051
- },
1052
- publishConfig: {
1053
- access: "public"
1054
- },
1055
- dependencies: {
1056
- "@mcp-b/webmcp-polyfill": "catalog:",
1057
- "@opentiny/next-sdk": "workspace:^",
1058
- "@page-agent/page-controller": "catalog:",
1059
- clipboardy: "^5.3.1",
1060
- commander: "^11.1.0",
1061
- json5: "^2.2.3",
1062
- marked: "^15.0.7",
1063
- picocolors: "^1.0.0",
1064
- "puppeteer-core": "^22.0.0"
1065
- },
1066
- devDependencies: {
1067
- "@types/node": "^20.0.0",
1068
- esbuild: "^0.25.0",
1069
- tsup: "^8.0.2",
1070
- tsx: "^4.0.0",
1071
- typescript: "^5.3.3"
1072
- }
1073
- };
1074
-
1075
3364
  // src/bin.ts
3365
+ init_package();
3366
+ function extractMode(argv) {
3367
+ let mode = process.env.WEBMCP_MODE === "wxt" ? "wxt" : "cdp";
3368
+ const filteredArgv = [];
3369
+ for (let i = 0; i < argv.length; i++) {
3370
+ if (argv[i] === "--mode" && argv[i + 1]) {
3371
+ const val = argv[i + 1].toLowerCase();
3372
+ if (val === "wxt" || val === "cdp") mode = val;
3373
+ i++;
3374
+ } else if (argv[i].startsWith("--mode=")) {
3375
+ const val = argv[i].split("=")[1]?.toLowerCase();
3376
+ if (val === "wxt" || val === "cdp") mode = val;
3377
+ } else {
3378
+ filteredArgv.push(argv[i]);
3379
+ }
3380
+ }
3381
+ return { mode, filteredArgv };
3382
+ }
3383
+ var { mode: CLI_MODE, filteredArgv: FILTERED_ARGV } = extractMode(process.argv);
1076
3384
  var program = new Command();
1077
3385
  function parseTabId(id) {
1078
3386
  if (!id) return void 0;
1079
3387
  return id;
1080
3388
  }
3389
+ function tryParseBrowserStateText(text) {
3390
+ const tryParse = (jsonStr) => {
3391
+ try {
3392
+ const parsed = JSON.parse(jsonStr);
3393
+ if (parsed && typeof parsed.content === "string") {
3394
+ return parsed;
3395
+ }
3396
+ } catch {
3397
+ }
3398
+ return null;
3399
+ };
3400
+ const direct = tryParse(text);
3401
+ if (direct) return direct;
3402
+ if (text.startsWith("\u6D4F\u89C8\u5668\u72B6\u6001: ")) {
3403
+ let jsonStr = text.substring("\u6D4F\u89C8\u5668\u72B6\u6001: ".length);
3404
+ const firstNewline = jsonStr.indexOf("\n");
3405
+ if (firstNewline !== -1) {
3406
+ const maybeJson = jsonStr.substring(0, firstNewline);
3407
+ if (tryParse(maybeJson)) jsonStr = maybeJson;
3408
+ }
3409
+ return tryParse(jsonStr);
3410
+ }
3411
+ return null;
3412
+ }
1081
3413
  function cleanOldLogs(baseDir, logDir) {
1082
3414
  try {
1083
- const oldLogFile = path2.join(baseDir, "webmcp-cli.log");
1084
- if (fs2.existsSync(oldLogFile)) {
3415
+ const oldLogFile = path6.join(baseDir, "webmcp-cli.log");
3416
+ if (fs6.existsSync(oldLogFile)) {
1085
3417
  try {
1086
- fs2.unlinkSync(oldLogFile);
3418
+ fs6.unlinkSync(oldLogFile);
1087
3419
  } catch {
1088
3420
  }
1089
3421
  }
1090
- if (fs2.existsSync(baseDir)) {
1091
- const baseFiles = fs2.readdirSync(baseDir);
3422
+ if (fs6.existsSync(baseDir)) {
3423
+ const baseFiles = fs6.readdirSync(baseDir);
1092
3424
  for (const file of baseFiles) {
1093
3425
  if (/^webmcp-cli-\d{4}-\d{2}-\d{2}\.log$/.test(file)) {
1094
3426
  try {
1095
- fs2.unlinkSync(path2.join(baseDir, file));
3427
+ fs6.unlinkSync(path6.join(baseDir, file));
1096
3428
  } catch {
1097
3429
  }
1098
3430
  }
1099
3431
  }
1100
3432
  }
1101
- if (!fs2.existsSync(logDir)) return;
1102
- const files = fs2.readdirSync(logDir);
3433
+ if (!fs6.existsSync(logDir)) return;
3434
+ const files = fs6.readdirSync(logDir);
1103
3435
  const logFilePattern = /^webmcp-cli-(\d{4}-\d{2}-\d{2})\.log$/;
1104
3436
  const now = Date.now();
1105
3437
  const maxAgeMs = 7 * 24 * 60 * 60 * 1e3;
@@ -1111,7 +3443,7 @@ function cleanOldLogs(baseDir, logDir) {
1111
3443
  if (isNaN(fileDate)) continue;
1112
3444
  if (now - fileDate > maxAgeMs) {
1113
3445
  try {
1114
- fs2.unlinkSync(path2.join(logDir, file));
3446
+ fs6.unlinkSync(path6.join(logDir, file));
1115
3447
  } catch {
1116
3448
  }
1117
3449
  }
@@ -1133,23 +3465,11 @@ function formatLogResult(result) {
1133
3465
  let searchResultStr = "";
1134
3466
  if (clone && Array.isArray(clone.content) && clone.content.length > 0 && clone.content[0] && typeof clone.content[0].text === "string") {
1135
3467
  const text = clone.content[0].text;
1136
- if (text.startsWith("\u6D4F\u89C8\u5668\u72B6\u6001: ")) {
1137
- let jsonStr = text.substring("\u6D4F\u89C8\u5668\u72B6\u6001: ".length);
1138
- let extraAlerts = "";
1139
- const firstNewline = jsonStr.indexOf("\n");
1140
- if (firstNewline !== -1) {
1141
- extraAlerts = jsonStr.substring(firstNewline);
1142
- jsonStr = jsonStr.substring(0, firstNewline);
1143
- }
1144
- try {
1145
- const parsed = JSON.parse(jsonStr);
1146
- if (parsed && typeof parsed.content === "string") {
1147
- a11yTreeStr = parsed.content;
1148
- parsed.content = "[Formatted A11y Tree - See details below]";
1149
- clone.content[0].text = `\u6D4F\u89C8\u5668\u72B6\u6001: ${JSON.stringify(parsed, null, 2)}${extraAlerts}`;
1150
- }
1151
- } catch {
1152
- }
3468
+ const browserState = tryParseBrowserStateText(text);
3469
+ if (browserState) {
3470
+ a11yTreeStr = browserState.content;
3471
+ browserState.content = "[Formatted A11y Tree - See details below]";
3472
+ clone.content[0].text = JSON.stringify(browserState, null, 2);
1153
3473
  } else if (text.includes("A11y Tree \u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22\u7ED3\u679C") || text.includes("\u65E0\u969C\u788D\u6811\u641C\u7D22") || text.includes("\u547D\u4E2D\u884C") || text.includes("context:")) {
1154
3474
  searchResultStr = text;
1155
3475
  clone.content[0].text = "[Search A11y Tree Result - See details below]";
@@ -1179,13 +3499,13 @@ ${searchResultStr}`;
1179
3499
  }
1180
3500
  function writeLog(commandName, args, result, error) {
1181
3501
  try {
1182
- const baseDir = process.env.WEBMCP_WORKSPACE || path2.join(os3.homedir(), ".webmcp_chrome_profile");
1183
- const logDir = path2.join(baseDir, "logs");
1184
- if (!fs2.existsSync(logDir)) {
1185
- fs2.mkdirSync(logDir, { recursive: true });
3502
+ const baseDir = process.env.WEBMCP_WORKSPACE || path6.join(os5.homedir(), ".webmcp_chrome_profile");
3503
+ const logDir = path6.join(baseDir, "logs");
3504
+ if (!fs6.existsSync(logDir)) {
3505
+ fs6.mkdirSync(logDir, { recursive: true });
1186
3506
  }
1187
3507
  const todayStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1188
- const logFile = path2.join(logDir, `webmcp-cli-${todayStr}.log`);
3508
+ const logFile = path6.join(logDir, `webmcp-cli-${todayStr}.log`);
1189
3509
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1190
3510
  let logText = `========================================
1191
3511
  `;
@@ -1211,7 +3531,7 @@ ${error instanceof Error ? error.stack : String(error)}
1211
3531
  logText += `========================================
1212
3532
 
1213
3533
  `;
1214
- fs2.appendFileSync(logFile, logText, "utf-8");
3534
+ fs6.appendFileSync(logFile, logText, "utf-8");
1215
3535
  cleanOldLogs(baseDir, logDir);
1216
3536
  } catch (e) {
1217
3537
  }
@@ -1222,7 +3542,9 @@ function handleCommandError(error, commandName, args) {
1222
3542
  console.error(pc2.red(`Error executing ${commandName} command: ${msg}`));
1223
3543
  process.exit(1);
1224
3544
  }
1225
- program.name("webmcp-cli").description("WebMCP CLI for interacting with browser via CDP").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").hook("preAction", (thisCommand) => {
3545
+ program.name("webmcp-cli").description(
3546
+ "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) => {
1226
3548
  const opts = thisCommand.opts();
1227
3549
  if (opts.workspace) {
1228
3550
  process.env.WEBMCP_WORKSPACE = opts.workspace;
@@ -1334,5 +3656,174 @@ tabs.command("forward [tabid]").description("\u5C06\u5F53\u524D\u6216\u6307\u5B9
1334
3656
  handleCommandError(error, "tabs forward", args);
1335
3657
  }
1336
3658
  });
1337
- program.parse(process.argv);
3659
+ program.command("mcp").description("\u4EE5\u6807\u51C6 MCP Server \u6A21\u5F0F\u542F\u52A8\uFF08stdio \u4F20\u8F93\uFF0CCDP \u5E95\u5C42\uFF09\u3002\n\u4F7F\u7528 --mode wxt mcp \u5207\u6362\u5230 WXT \u6865\u63A5\u5E95\u5C42\u3002").option("--agent", "\u4EE5\u5B50\u4EE3\u7406\u6A21\u5F0F\u542F\u52A8\uFF08\u6536\u655B\u5DE5\u5177\u4E3A\u9AD8\u5C42\u59D4\u6258\u63A5\u53E3\uFF09").action(async (opts) => {
3660
+ try {
3661
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3662
+ await startMcpServer2({ mode: "cdp", agentMode: opts.agent ? "agent" : "tools" });
3663
+ } catch (error) {
3664
+ handleCommandError(error, "mcp", {});
3665
+ }
3666
+ });
3667
+ if (CLI_MODE === "wxt") {
3668
+ void handleWxtMode(FILTERED_ARGV.slice(2)).catch((error) => {
3669
+ handleCommandError(error, `wxt:${FILTERED_ARGV[2] ?? "help"}`, {});
3670
+ });
3671
+ } else {
3672
+ program.parse(FILTERED_ARGV);
3673
+ }
3674
+ async function handleWxtMode(args) {
3675
+ const command = args[0];
3676
+ if (command === "daemon" || command === "server") {
3677
+ const { runDaemonProcess: runDaemonProcess2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
3678
+ await runDaemonProcess2(args.slice(1));
3679
+ return;
3680
+ }
3681
+ if (command === "stop") {
3682
+ const { stopBridgeDaemon: stopBridgeDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
3683
+ const stopped = await stopBridgeDaemon2();
3684
+ console.log(stopped ? "\u2705 \u5DF2\u505C\u6B62\u540E\u53F0 WebSocket \u6865\u63A5\u670D\u52A1" : "\u2139\uFE0F \u5F53\u524D\u6CA1\u6709\u8FD0\u884C\u4E2D\u7684\u540E\u53F0\u6865\u63A5\u670D\u52A1");
3685
+ return;
3686
+ }
3687
+ if (command === "token") {
3688
+ const subArgs = args.slice(1);
3689
+ let wsPort2;
3690
+ const cleanArgs = [];
3691
+ for (let i = 0; i < subArgs.length; i++) {
3692
+ if ((subArgs[i] === "--ws-port" || subArgs[i] === "--port") && subArgs[i + 1]) {
3693
+ wsPort2 = parseInt(subArgs[++i], 10) || void 0;
3694
+ } else {
3695
+ cleanArgs.push(subArgs[i]);
3696
+ }
3697
+ }
3698
+ const action = cleanArgs[0];
3699
+ let tokenToSet;
3700
+ if (action === "set" && cleanArgs[1]) {
3701
+ tokenToSet = cleanArgs[1].trim();
3702
+ } else if (action && action !== "get" && !action.startsWith("-")) {
3703
+ tokenToSet = action.trim();
3704
+ }
3705
+ const { resolveOrCreateAuthToken: resolveOrCreateAuthToken2 } = await Promise.resolve().then(() => (init_bridge_client(), bridge_client_exports));
3706
+ if (tokenToSet) {
3707
+ resolveOrCreateAuthToken2(tokenToSet);
3708
+ console.log("\u2705 Token \u5DF2\u6210\u529F\u4FDD\u5B58\u81F3 ~/.robot-wxt/bridge-token");
3709
+ const { stopBridgeDaemon: stopBridgeDaemon2, ensureBridgeDaemon: ensureBridgeDaemon3 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
3710
+ await stopBridgeDaemon2(wsPort2);
3711
+ await ensureBridgeDaemon3({ token: tokenToSet, port: wsPort2 });
3712
+ console.log("\u{1F4A1} \u540E\u7EED\u6240\u6709 CLI \u547D\u4EE4\u4E0E MCP \u5DE5\u5177\u5747\u5C06\u81EA\u52A8\u4F7F\u7528\u6B64\u51ED\u636E\uFF0C\u65E0\u9700\u91CD\u590D\u8F93\u5165\u3002");
3713
+ return;
3714
+ }
3715
+ console.log(resolveOrCreateAuthToken2());
3716
+ return;
3717
+ }
3718
+ if (command === "mcp") {
3719
+ const { startMcpServer: startMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
3720
+ const isAgent = args.includes("--agent") || args.includes("--mode=agent");
3721
+ const tokenIdx = args.indexOf("--token");
3722
+ const token2 = tokenIdx !== -1 ? args[tokenIdx + 1] : void 0;
3723
+ const wsPortIdx = args.indexOf("--ws-port");
3724
+ const wsPort2 = wsPortIdx !== -1 ? parseInt(args[wsPortIdx + 1], 10) || void 0 : void 0;
3725
+ await startMcpServer2({ mode: "wxt", agentMode: isAgent ? "agent" : "tools", token: token2, wsPort: wsPort2 });
3726
+ return;
3727
+ }
3728
+ if (!command || command === "--help" || command === "-h") {
3729
+ printWxtHelp();
3730
+ return;
3731
+ }
3732
+ let token;
3733
+ let wsPort;
3734
+ const commandArgs = [];
3735
+ for (let i = 1; i < args.length; i++) {
3736
+ if (args[i] === "--token" && args[i + 1]) {
3737
+ token = args[++i];
3738
+ } else if (args[i] === "--ws-port" && args[i + 1]) {
3739
+ wsPort = parseInt(args[++i], 10) || void 0;
3740
+ } else {
3741
+ commandArgs.push(args[i]);
3742
+ }
3743
+ }
3744
+ const { ensureBridgeDaemon: ensureBridgeDaemon2 } = await Promise.resolve().then(() => (init_daemon(), daemon_exports));
3745
+ const isJson = commandArgs.includes("--json");
3746
+ await ensureBridgeDaemon2({ token, port: wsPort, silent: isJson });
3747
+ const { WxtBrowserAdapter: WxtBrowserAdapter2 } = await Promise.resolve().then(() => (init_wxt_adapter(), wxt_adapter_exports));
3748
+ const adapter = new WxtBrowserAdapter2(token, wsPort);
3749
+ await adapter.ensureConnected();
3750
+ try {
3751
+ switch (command) {
3752
+ case "state": {
3753
+ const tabIdx = commandArgs.indexOf("--tab");
3754
+ const tabId = tabIdx !== -1 ? commandArgs[tabIdx + 1] : void 0;
3755
+ const result = await adapter.getState(tabId ? parseInt(tabId, 10) : void 0);
3756
+ console.log(JSON.stringify(result, null, 2));
3757
+ break;
3758
+ }
3759
+ case "run": {
3760
+ const toolName = commandArgs.find((a) => !a.startsWith("-"));
3761
+ const argsStr = commandArgs.slice(commandArgs.indexOf(toolName) + 1).find((a) => !a.startsWith("-"));
3762
+ if (!toolName) {
3763
+ console.error("[\u9519\u8BEF] \u8BF7\u63D0\u4F9B\u5DE5\u5177\u540D\u79F0");
3764
+ process.exit(1);
3765
+ }
3766
+ const toolArgs = argsStr ? JSON.parse(argsStr) : {};
3767
+ const result = await adapter.callTool(toolName, toolArgs);
3768
+ console.log(JSON.stringify(result, null, 2));
3769
+ break;
3770
+ }
3771
+ case "tabs": {
3772
+ const tabAction = commandArgs[0];
3773
+ const tabArgs = commandArgs.slice(1);
3774
+ const url = tabArgs.find((a) => a.startsWith("http"));
3775
+ const tabIdVal = tabArgs.find((a) => !a.startsWith("-") && !a.startsWith("http"));
3776
+ const result = await adapter.tabs(tabAction, {
3777
+ url,
3778
+ tabId: tabIdVal ? parseInt(tabIdVal, 10) : void 0
3779
+ });
3780
+ console.log(JSON.stringify(result, null, 2));
3781
+ break;
3782
+ }
3783
+ case "agent": {
3784
+ const { handleAgentCommand: handleAgentCommand2 } = await Promise.resolve().then(() => (init_agent(), agent_exports));
3785
+ await handleAgentCommand2(adapter, commandArgs);
3786
+ break;
3787
+ }
3788
+ case "skills":
3789
+ case "skill": {
3790
+ const { handleSkillsCommand: handleSkillsCommand2 } = await Promise.resolve().then(() => (init_skills(), skills_exports));
3791
+ await handleSkillsCommand2(adapter, commandArgs);
3792
+ break;
3793
+ }
3794
+ default:
3795
+ console.error(`[\u9519\u8BEF] WXT \u6A21\u5F0F\u4E0B\u672A\u77E5\u547D\u4EE4: ${command}`);
3796
+ printWxtHelp();
3797
+ process.exit(1);
3798
+ }
3799
+ } finally {
3800
+ await adapter.dispose();
3801
+ }
3802
+ }
3803
+ function printWxtHelp() {
3804
+ console.log(`
3805
+ webmcp-cli --mode wxt \u2014 WXT \u6865\u63A5\u6A21\u5F0F\uFF08\u901A\u8FC7 Chrome \u6269\u5C55\u8FDE\u63A5\uFF0C\u590D\u7528\u7528\u6237\u6570\u636E\uFF09
3806
+
3807
+ \u547D\u4EE4\uFF1A
3808
+ state \u83B7\u53D6\u5F53\u524D\u6D3B\u8DC3 tab \u4FE1\u606F\u4E0E\u9875\u9762\u5DE5\u5177\u5217\u8868
3809
+ run <tool> [args] \u8C03\u7528\u9875\u9762 WebMCP \u5DE5\u5177
3810
+ tabs <action> [...] \u7BA1\u7406\u6807\u7B7E\u9875\uFF08open/close/switch/back/forward\uFF09
3811
+ agent run "<\u6307\u4EE4>" \u59D4\u6258 Tiny Robot \u5B50\u4EE3\u7406\u81EA\u4E3B\u6267\u884C\u9AD8\u5C42\u4EFB\u52A1
3812
+ skills list \u5217\u51FA\u5DF2\u6CE8\u518C\u7684 Skills \u6280\u80FD
3813
+ skills get <name> \u8BFB\u53D6\u6307\u5B9A\u6280\u80FD\u7684\u8BE6\u7EC6\u6307\u4EE4
3814
+ token [get|set <t>] \u67E5\u770B\u6216\u4FDD\u5B58\u8BA4\u8BC1 Token
3815
+ stop \u505C\u6B62\u540E\u53F0 WebSocket \u6865\u63A5\u670D\u52A1
3816
+ mcp \u4EE5 MCP Server \u6A21\u5F0F\u542F\u52A8
3817
+
3818
+ \u9009\u9879\uFF1A
3819
+ --token <token> \u8BA4\u8BC1 Token\uFF08\u4F18\u5148\u4E8E ~/.robot-wxt/bridge-token\uFF09
3820
+ --ws-port <port> WS \u6865\u63A5\u7AEF\u53E3\uFF08\u9ED8\u8BA4 18999\uFF09
3821
+ --json \u8F93\u51FA JSON \u683C\u5F0F
3822
+
3823
+ \u793A\u4F8B\uFF1A
3824
+ webmcp-cli --mode wxt state
3825
+ webmcp-cli --mode wxt agent run "\u5E2E\u6211\u628A\u5546\u54C1\u52A0\u5165\u8D2D\u7269\u8F66"
3826
+ webmcp-cli --mode wxt mcp --agent
3827
+ `.trim());
3828
+ }
1338
3829
  //# sourceMappingURL=bin.js.map