@opentiny/webmcp-cli 0.0.1-alpha.3 → 0.0.1-alpha.4

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,14 +1,10 @@
1
1
  #!/usr/bin/env node
2
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
- }) : x)(function(x) {
5
- if (typeof require !== "undefined") return require.apply(this, arguments);
6
- throw Error('Dynamic require of "' + x + '" is not supported');
7
- });
8
2
 
9
3
  // src/bin.ts
10
4
  import { Command } from "commander";
11
5
  import pc2 from "picocolors";
6
+ import { readFileSync } from "fs";
7
+ import { resolve } from "path";
12
8
 
13
9
  // src/browser.ts
14
10
  import puppeteer from "puppeteer-core";
@@ -16,20 +12,34 @@ import pc from "picocolors";
16
12
  import fs from "fs";
17
13
  import os from "os";
18
14
  import path from "path";
15
+ import http from "http";
19
16
  import { spawn } from "child_process";
20
17
  import { fileURLToPath } from "url";
21
18
  var __filename = fileURLToPath(import.meta.url);
22
19
  var __dirname = path.dirname(__filename);
23
20
  var CDP_PORT = 9222;
24
21
  var CDP_URL = `http://localhost:${CDP_PORT}`;
25
- async function fetchWithTimeout(url, timeoutMs = 1500) {
26
- const controller = new AbortController();
27
- const id = setTimeout(() => controller.abort(), timeoutMs);
28
- try {
29
- return await fetch(url, { signal: controller.signal });
30
- } finally {
31
- clearTimeout(id);
32
- }
22
+ function httpGet(url, timeoutMs = 1500) {
23
+ return new Promise((resolve2, reject) => {
24
+ const req = http.get(url, { timeout: timeoutMs }, (res) => {
25
+ let data = "";
26
+ res.on("data", (chunk) => {
27
+ data += chunk.toString();
28
+ });
29
+ res.on("end", () => {
30
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
31
+ resolve2(data);
32
+ } else {
33
+ reject(new Error(`HTTP ${res.statusCode}`));
34
+ }
35
+ });
36
+ });
37
+ req.on("error", reject);
38
+ req.on("timeout", () => {
39
+ req.destroy();
40
+ reject(new Error("request timeout"));
41
+ });
42
+ });
33
43
  }
34
44
  function promiseWithTimeout(promise, timeoutMs = 5e3, errorMsg = "Operation timed out") {
35
45
  let timeoutId;
@@ -45,8 +55,8 @@ function promiseWithTimeout(promise, timeoutMs = 5e3, errorMsg = "Operation time
45
55
  async function checkCdpReady(url, retries = 3) {
46
56
  for (let i = 0; i < retries; i++) {
47
57
  try {
48
- const res = await fetchWithTimeout(url, 1500);
49
- if (res.ok) return true;
58
+ await httpGet(url, 1500);
59
+ return true;
50
60
  } catch {
51
61
  }
52
62
  if (i < retries - 1) {
@@ -55,45 +65,6 @@ async function checkCdpReady(url, retries = 3) {
55
65
  }
56
66
  return false;
57
67
  }
58
- async function killProcessOnPortIfZombie(port) {
59
- const isResponding = await checkCdpReady(`http://127.0.0.1:${port}/json/version`, 1);
60
- if (isResponding) {
61
- console.log(pc.green(`connectBrowser: \u7AEF\u53E3 ${port} \u4E0A\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B\u4ECD\u5728\u6B63\u5E38\u54CD\u5E94\uFF0C\u8DF3\u8FC7\u5F3A\u6740\uFF0C\u5C1D\u8BD5\u76F4\u63A5\u63A5\u7BA1\u3002`));
62
- return;
63
- }
64
- try {
65
- const platform = os.platform();
66
- const { execSync } = __require("child_process");
67
- if (platform === "darwin" || platform === "linux") {
68
- console.log(pc.yellow(`\u6B63\u5728\u68C0\u6D4B\u5E76\u6E05\u7406\u5360\u7528 ${port} \u7AEF\u53E3\u7684\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B...`));
69
- const pids = execSync(`lsof -t -i :${port}`).toString().trim();
70
- if (pids) {
71
- console.log(pc.yellow(`\u53D1\u73B0\u50F5\u5C38 PID: ${pids.split("\n").join(", ")}\uFF0C\u6B63\u5728\u5F3A\u5236\u7EC8\u6B62...`));
72
- execSync(`kill -9 ${pids.split("\n").join(" ")}`);
73
- console.log(pc.green(`\u6210\u529F\u6E05\u7406\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B`));
74
- }
75
- } else if (platform === "win32") {
76
- console.log(pc.yellow(`\u6B63\u5728\u68C0\u6D4B\u5E76\u6E05\u7406 Windows \u4E0A\u5360\u7528 ${port} \u7AEF\u53E3\u7684\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B...`));
77
- const output = execSync(`netstat -ano | findstr :${port}`).toString().trim();
78
- if (output) {
79
- const lines = output.split("\n");
80
- const pids = /* @__PURE__ */ new Set();
81
- lines.forEach((line) => {
82
- const parts = line.trim().split(/\s+/);
83
- const pid = parts[parts.length - 1];
84
- if (pid && /^\d+$/.test(pid) && pid !== "0") {
85
- pids.add(pid);
86
- }
87
- });
88
- pids.forEach((pid) => {
89
- console.log(pc.yellow(`\u53D1\u73B0 Windows \u6B8B\u7559\u50F5\u5C38 PID: ${pid}\uFF0C\u6B63\u5728\u5F3A\u5236\u7EC8\u6B62...`));
90
- execSync(`taskkill /F /PID ${pid}`);
91
- });
92
- }
93
- }
94
- } catch (e) {
95
- }
96
- }
97
68
  function getDefaultBrowserPath() {
98
69
  const platform = os.platform();
99
70
  if (platform === "darwin") {
@@ -156,20 +127,18 @@ async function startBrowserInBackground() {
156
127
  child.unref();
157
128
  for (let i = 0; i < 20; i++) {
158
129
  try {
159
- const urls = [`http://localhost:${CDP_PORT}/json/version`, `http://127.0.0.1:${CDP_PORT}/json/version`];
130
+ const urls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
160
131
  for (const url of urls) {
161
132
  try {
162
- const response = await fetchWithTimeout(url, 1e3);
163
- if (response.ok) {
164
- console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
165
- return;
166
- }
133
+ await httpGet(url, 1e3);
134
+ console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
135
+ return;
167
136
  } catch (err) {
168
137
  }
169
138
  }
170
139
  } catch (e) {
171
140
  }
172
- await new Promise((resolve) => setTimeout(resolve, 500));
141
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
173
142
  }
174
143
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
175
144
  }
@@ -190,90 +159,51 @@ async function connectBrowser() {
190
159
  return false;
191
160
  }
192
161
  };
193
- try {
194
- const is127Ready = await checkCdpReady(`http://127.0.0.1:${CDP_PORT}/json/version`, 3);
195
- if (!is127Ready) {
196
- throw new Error("127.0.0.1 CDP port not responding");
162
+ const portAddresses = [
163
+ `http://127.0.0.1:${CDP_PORT}/json/version`,
164
+ `http://localhost:${CDP_PORT}/json/version`
165
+ ];
166
+ for (const addr of portAddresses) {
167
+ const isReady = await checkCdpReady(addr, 2);
168
+ if (!isReady) continue;
169
+ const browserURL = addr.replace("/json/version", "");
170
+ try {
171
+ console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
172
+ const browser2 = await promiseWithTimeout(
173
+ puppeteer.connect({ browserURL, defaultViewport: null, targetFilter }),
174
+ 1e4,
175
+ `puppeteer.connect to ${browserURL} timed out`
176
+ );
177
+ console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
178
+ return browser2;
179
+ } catch {
197
180
  }
198
- console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
199
- const browser2 = await promiseWithTimeout(
200
- puppeteer.connect({
201
- browserURL: `http://127.0.0.1:${CDP_PORT}`,
202
- defaultViewport: null,
203
- targetFilter
204
- }),
205
- 1e4,
206
- "puppeteer.connect to 127.0.0.1 timed out"
207
- );
208
- console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
209
- return browser2;
210
- } catch (error) {
181
+ }
182
+ console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${CDP_PORT} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
183
+ try {
184
+ await startBrowserInBackground();
185
+ } catch (launchError) {
186
+ const msg = launchError instanceof Error ? launchError.message : String(launchError);
187
+ console.error(pc.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
188
+ throw new Error("Browser launch failed.");
189
+ }
190
+ for (const addr of portAddresses) {
191
+ const isReady = await checkCdpReady(addr, 3);
192
+ if (!isReady) continue;
193
+ const browserURL = addr.replace("/json/version", "");
211
194
  try {
212
- const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
213
- if (!isLocalhostReady) {
214
- throw new Error("localhost CDP port not responding");
215
- }
216
- console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
195
+ console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
217
196
  const browser2 = await promiseWithTimeout(
218
- puppeteer.connect({
219
- browserURL: `http://localhost:${CDP_PORT}`,
220
- defaultViewport: null,
221
- targetFilter
222
- }),
197
+ puppeteer.connect({ browserURL, defaultViewport: null, targetFilter }),
223
198
  1e4,
224
- "puppeteer.connect to localhost timed out"
199
+ `puppeteer.connect to ${browserURL} after launch timed out`
225
200
  );
226
- console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
201
+ console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
227
202
  return browser2;
228
- } catch (error2) {
229
- console.log(pc.yellow(`connectBrowser: \u8FDE\u63A5\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5\u5524\u8D77\u6D4F\u89C8\u5668\u3002\u9519\u8BEF\u539F\u56E0: ${error2 instanceof Error ? error2.message : String(error2)}`));
230
- try {
231
- await killProcessOnPortIfZombie(CDP_PORT);
232
- await startBrowserInBackground();
233
- try {
234
- const is127Ready = await checkCdpReady(`http://127.0.0.1:${CDP_PORT}/json/version`, 3);
235
- if (!is127Ready) {
236
- throw new Error("127.0.0.1 CDP port not responding after launch");
237
- }
238
- console.log(pc.yellow("connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
239
- const browser2 = await promiseWithTimeout(
240
- puppeteer.connect({
241
- browserURL: `http://127.0.0.1:${CDP_PORT}`,
242
- defaultViewport: null,
243
- targetFilter
244
- }),
245
- 1e4,
246
- "puppeteer.connect to 127.0.0.1 after launch timed out"
247
- );
248
- console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
249
- return browser2;
250
- } catch (e) {
251
- const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
252
- if (!isLocalhostReady) {
253
- throw new Error("localhost CDP port not responding after launch");
254
- }
255
- console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
256
- const browser2 = await promiseWithTimeout(
257
- puppeteer.connect({
258
- browserURL: `http://localhost:${CDP_PORT}`,
259
- defaultViewport: null,
260
- targetFilter
261
- }),
262
- 1e4,
263
- "puppeteer.connect to localhost after launch timed out"
264
- );
265
- console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
266
- return browser2;
267
- }
268
- } catch (launchError) {
269
- const msg = launchError instanceof Error ? launchError.message : String(launchError);
270
- console.error(pc.red(`\u65E0\u6CD5\u8FDE\u63A5\u6216\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
271
- console.error(pc.yellow(`\u{1F4A1} \u63D0\u793A\uFF1A\u7531\u4E8E\u6211\u4EEC\u8981\u4F7F\u7528\u4F60\u65E5\u5E38\u7684\u9ED8\u8BA4\u6D4F\u89C8\u5668\uFF08\u5305\u542B\u4F60\u7684\u4E66\u7B7E\u548C\u767B\u5F55\u6001\uFF09\uFF0C\u5982\u679C\u4F60\u7684 Chrome/Edge \u76EE\u524D\u6B63\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u5B83\u4F1A\u62D2\u7EDD\u4F7F\u7528\u5E26\u6709\u8C03\u8BD5\u7AEF\u53E3\u7684\u65B0\u53C2\u6570\u542F\u52A8\u3002`));
272
- console.error(pc.yellow(`\u{1F449} \u89E3\u51B3\u529E\u6CD5\uFF1A\u8BF7\u5148\u5B8C\u5168\u9000\u51FA\u5F53\u524D\u7684 Chrome \u6216 Edge \u6D4F\u89C8\u5668\uFF08\u5728 Mac \u4E0A\u6309 Cmd+Q\uFF0CWindows \u4E0A\u53F3\u952E\u4EFB\u52A1\u680F\u56FE\u6807\u9000\u51FA\uFF09\uFF0C\u7136\u540E\u518D\u91CD\u65B0\u8FD0\u884C\u547D\u4EE4\u3002`));
273
- throw new Error("Browser connection failed.");
274
- }
203
+ } catch {
275
204
  }
276
205
  }
206
+ 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`);
277
207
  }
278
208
  async function getPageTargetId(page2) {
279
209
  const session = await page2.target().createCDPSession();
@@ -351,14 +281,12 @@ async function getTargetPage(browser2, tabid2) {
351
281
  let activeTargetId = null;
352
282
  for (const url of urls) {
353
283
  try {
354
- const res = await fetchWithTimeout(url, 1e3);
355
- if (res.ok) {
356
- const targetsData = await res.json();
357
- const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
358
- if (active) {
359
- activeTargetId = active.id;
360
- break;
361
- }
284
+ const body = await httpGet(url, 1e3);
285
+ const targetsData = JSON.parse(body);
286
+ const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
287
+ if (active) {
288
+ activeTargetId = active.id;
289
+ break;
362
290
  }
363
291
  } catch {
364
292
  }
@@ -404,7 +332,7 @@ async function injectWebMCPPolyfillAndTools(page2, force = false) {
404
332
  const msg = err instanceof Error ? err.message : String(err);
405
333
  throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
406
334
  }
407
- await new Promise((resolve) => setTimeout(resolve, 300));
335
+ await new Promise((resolve2) => setTimeout(resolve2, 300));
408
336
  }
409
337
  await injectDomainTools(page2);
410
338
  }
@@ -492,7 +420,7 @@ async function stateCommand({ tabid: tabid2 }) {
492
420
  if (pUrl.startsWith("devtools://")) return null;
493
421
  const pTitle = await Promise.race([
494
422
  p.title().catch(() => "Unknown"),
495
- new Promise((resolve) => setTimeout(() => resolve("Unknown"), 500))
423
+ new Promise((resolve2) => setTimeout(() => resolve2("Unknown"), 500))
496
424
  ]);
497
425
  return {
498
426
  // 使用真实的 Chrome target ID,而非数组下标
@@ -598,7 +526,7 @@ async function tabsOpenCommand(url) {
598
526
  url = normalizeUrl(url);
599
527
  await page2.goto(url, { waitUntil: "domcontentloaded" });
600
528
  await injectIntoPage(page2);
601
- await new Promise((resolve) => setTimeout(resolve, 500));
529
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
602
530
  return {
603
531
  success: true,
604
532
  tabid: await getPageTargetId(page2),
@@ -699,7 +627,7 @@ async function tabsForwardCommand(tabid2) {
699
627
  // package.json
700
628
  var package_default = {
701
629
  name: "@opentiny/webmcp-cli",
702
- version: "0.0.1-alpha.3",
630
+ version: "0.0.1-alpha.4",
703
631
  type: "module",
704
632
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
705
633
  main: "dist/index.js",
@@ -761,6 +689,19 @@ function handleCommandError(error, commandName) {
761
689
  console.error(pc2.red(`Error executing ${commandName} command: ${msg}`));
762
690
  process.exit(1);
763
691
  }
692
+ function expandFileRefs(argsJson) {
693
+ return argsJson.replace(/"@(base64)?file:([^"]+)"/g, (_match, base64Flag, filePath) => {
694
+ const absPath = resolve(process.cwd(), filePath);
695
+ let content;
696
+ try {
697
+ content = readFileSync(absPath, "utf-8");
698
+ } catch (e) {
699
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}": ${e.message}`);
700
+ }
701
+ const value = base64Flag ? Buffer.from(content, "utf-8").toString("base64") : content;
702
+ return JSON.stringify(value);
703
+ });
704
+ }
764
705
  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) => {
765
706
  const opts = thisCommand.opts();
766
707
  if (opts.workspace) {
@@ -775,11 +716,31 @@ program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D
775
716
  handleCommandError(error, "state");
776
717
  }
777
718
  });
778
- program.command("run <toolName> <argsJson>").description("\u5411\u6307\u5B9A\u9875\u7B7E\u8C03\u7528\u6307\u5B9A\u7684 WebMCP \u5DE5\u5177\u6267\u884C\u64CD\u4F5C").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (toolName, argsJson, options) => {
719
+ program.command("run <toolName> [argsJson]").description("\u5411\u6307\u5B9A\u9875\u7B7E\u8C03\u7528\u6307\u5B9A\u7684 WebMCP \u5DE5\u5177\u6267\u884C\u64CD\u4F5C").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").option(
720
+ "-f, --file <path>",
721
+ `\u4ECE\u6307\u5B9A .json \u6587\u4EF6\u8BFB\u53D6\u6574\u4E2A argsJson\uFF08\u6587\u4EF6\u5185\u5BB9\u5373\u4E3A\u53C2\u6570 JSON\uFF09\u3002
722
+ \u5982\u9700\u5728 argsJson \u4E2D\u5185\u8054\u5F15\u7528\u6587\u4EF6\uFF0C\u4F7F\u7528\u5360\u4F4D\u7B26\u8BED\u6CD5\uFF1A
723
+ @file:<path> \u8BFB\u53D6\u6587\u4EF6\u539F\u59CB\u6587\u672C
724
+ @base64file:<path> \u8BFB\u53D6\u6587\u4EF6\u5E76 Base64 \u7F16\u7801
725
+ \u793A\u4F8B\uFF1Awebmcp-cli run mytool '{"content":"@base64file:./doc.md"}'`
726
+ ).action(async (toolName, argsJson, options) => {
779
727
  try {
728
+ let finalArgsJson = argsJson;
729
+ if (options.file) {
730
+ const filePath = resolve(process.cwd(), options.file);
731
+ try {
732
+ finalArgsJson = readFileSync(filePath, "utf-8").trim();
733
+ } catch (e) {
734
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6 "${filePath}": ${e.message}`);
735
+ }
736
+ }
737
+ if (!finalArgsJson) {
738
+ throw new Error("\u5FC5\u987B\u63D0\u4F9B argsJson \u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
739
+ }
740
+ finalArgsJson = expandFileRefs(finalArgsJson);
780
741
  const result2 = await runCommand({
781
742
  toolName,
782
- argsJson,
743
+ argsJson: finalArgsJson,
783
744
  tabid: parseTabId(options.tabid)
784
745
  });
785
746
  console.log(JSON.stringify(result2, null, 2));