@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.cjs CHANGED
@@ -26,6 +26,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/bin.ts
27
27
  var import_commander = require("commander");
28
28
  var import_picocolors2 = __toESM(require("picocolors"), 1);
29
+ var import_fs2 = require("fs");
30
+ var import_path2 = require("path");
29
31
 
30
32
  // src/browser.ts
31
33
  var import_puppeteer_core = __toESM(require("puppeteer-core"), 1);
@@ -33,6 +35,7 @@ var import_picocolors = __toESM(require("picocolors"), 1);
33
35
  var import_fs = __toESM(require("fs"), 1);
34
36
  var import_os = __toESM(require("os"), 1);
35
37
  var import_path = __toESM(require("path"), 1);
38
+ var import_http = __toESM(require("http"), 1);
36
39
  var import_child_process = require("child_process");
37
40
  var import_url = require("url");
38
41
  var import_meta = {};
@@ -40,14 +43,27 @@ var __filename = (0, import_url.fileURLToPath)(import_meta.url);
40
43
  var __dirname = import_path.default.dirname(__filename);
41
44
  var CDP_PORT = 9222;
42
45
  var CDP_URL = `http://localhost:${CDP_PORT}`;
43
- async function fetchWithTimeout(url, timeoutMs = 1500) {
44
- const controller = new AbortController();
45
- const id = setTimeout(() => controller.abort(), timeoutMs);
46
- try {
47
- return await fetch(url, { signal: controller.signal });
48
- } finally {
49
- clearTimeout(id);
50
- }
46
+ function httpGet(url, timeoutMs = 1500) {
47
+ return new Promise((resolve2, reject) => {
48
+ const req = import_http.default.get(url, { timeout: timeoutMs }, (res) => {
49
+ let data = "";
50
+ res.on("data", (chunk) => {
51
+ data += chunk.toString();
52
+ });
53
+ res.on("end", () => {
54
+ if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
55
+ resolve2(data);
56
+ } else {
57
+ reject(new Error(`HTTP ${res.statusCode}`));
58
+ }
59
+ });
60
+ });
61
+ req.on("error", reject);
62
+ req.on("timeout", () => {
63
+ req.destroy();
64
+ reject(new Error("request timeout"));
65
+ });
66
+ });
51
67
  }
52
68
  function promiseWithTimeout(promise, timeoutMs = 5e3, errorMsg = "Operation timed out") {
53
69
  let timeoutId;
@@ -63,8 +79,8 @@ function promiseWithTimeout(promise, timeoutMs = 5e3, errorMsg = "Operation time
63
79
  async function checkCdpReady(url, retries = 3) {
64
80
  for (let i = 0; i < retries; i++) {
65
81
  try {
66
- const res = await fetchWithTimeout(url, 1500);
67
- if (res.ok) return true;
82
+ await httpGet(url, 1500);
83
+ return true;
68
84
  } catch {
69
85
  }
70
86
  if (i < retries - 1) {
@@ -73,45 +89,6 @@ async function checkCdpReady(url, retries = 3) {
73
89
  }
74
90
  return false;
75
91
  }
76
- async function killProcessOnPortIfZombie(port) {
77
- const isResponding = await checkCdpReady(`http://127.0.0.1:${port}/json/version`, 1);
78
- if (isResponding) {
79
- console.log(import_picocolors.default.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`));
80
- return;
81
- }
82
- try {
83
- const platform = import_os.default.platform();
84
- const { execSync } = require("child_process");
85
- if (platform === "darwin" || platform === "linux") {
86
- console.log(import_picocolors.default.yellow(`\u6B63\u5728\u68C0\u6D4B\u5E76\u6E05\u7406\u5360\u7528 ${port} \u7AEF\u53E3\u7684\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B...`));
87
- const pids = execSync(`lsof -t -i :${port}`).toString().trim();
88
- if (pids) {
89
- console.log(import_picocolors.default.yellow(`\u53D1\u73B0\u50F5\u5C38 PID: ${pids.split("\n").join(", ")}\uFF0C\u6B63\u5728\u5F3A\u5236\u7EC8\u6B62...`));
90
- execSync(`kill -9 ${pids.split("\n").join(" ")}`);
91
- console.log(import_picocolors.default.green(`\u6210\u529F\u6E05\u7406\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B`));
92
- }
93
- } else if (platform === "win32") {
94
- console.log(import_picocolors.default.yellow(`\u6B63\u5728\u68C0\u6D4B\u5E76\u6E05\u7406 Windows \u4E0A\u5360\u7528 ${port} \u7AEF\u53E3\u7684\u6B8B\u7559\u50F5\u5C38\u8FDB\u7A0B...`));
95
- const output = execSync(`netstat -ano | findstr :${port}`).toString().trim();
96
- if (output) {
97
- const lines = output.split("\n");
98
- const pids = /* @__PURE__ */ new Set();
99
- lines.forEach((line) => {
100
- const parts = line.trim().split(/\s+/);
101
- const pid = parts[parts.length - 1];
102
- if (pid && /^\d+$/.test(pid) && pid !== "0") {
103
- pids.add(pid);
104
- }
105
- });
106
- pids.forEach((pid) => {
107
- console.log(import_picocolors.default.yellow(`\u53D1\u73B0 Windows \u6B8B\u7559\u50F5\u5C38 PID: ${pid}\uFF0C\u6B63\u5728\u5F3A\u5236\u7EC8\u6B62...`));
108
- execSync(`taskkill /F /PID ${pid}`);
109
- });
110
- }
111
- }
112
- } catch (e) {
113
- }
114
- }
115
92
  function getDefaultBrowserPath() {
116
93
  const platform = import_os.default.platform();
117
94
  if (platform === "darwin") {
@@ -174,20 +151,18 @@ async function startBrowserInBackground() {
174
151
  child.unref();
175
152
  for (let i = 0; i < 20; i++) {
176
153
  try {
177
- const urls = [`http://localhost:${CDP_PORT}/json/version`, `http://127.0.0.1:${CDP_PORT}/json/version`];
154
+ const urls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
178
155
  for (const url of urls) {
179
156
  try {
180
- const response = await fetchWithTimeout(url, 1e3);
181
- if (response.ok) {
182
- console.log(import_picocolors.default.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
183
- return;
184
- }
157
+ await httpGet(url, 1e3);
158
+ console.log(import_picocolors.default.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
159
+ return;
185
160
  } catch (err) {
186
161
  }
187
162
  }
188
163
  } catch (e) {
189
164
  }
190
- await new Promise((resolve) => setTimeout(resolve, 500));
165
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
191
166
  }
192
167
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
193
168
  }
@@ -208,90 +183,51 @@ async function connectBrowser() {
208
183
  return false;
209
184
  }
210
185
  };
211
- try {
212
- const is127Ready = await checkCdpReady(`http://127.0.0.1:${CDP_PORT}/json/version`, 3);
213
- if (!is127Ready) {
214
- throw new Error("127.0.0.1 CDP port not responding");
186
+ const portAddresses = [
187
+ `http://127.0.0.1:${CDP_PORT}/json/version`,
188
+ `http://localhost:${CDP_PORT}/json/version`
189
+ ];
190
+ for (const addr of portAddresses) {
191
+ const isReady = await checkCdpReady(addr, 2);
192
+ if (!isReady) continue;
193
+ const browserURL = addr.replace("/json/version", "");
194
+ try {
195
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
196
+ const browser2 = await promiseWithTimeout(
197
+ import_puppeteer_core.default.connect({ browserURL, defaultViewport: null, targetFilter }),
198
+ 1e4,
199
+ `puppeteer.connect to ${browserURL} timed out`
200
+ );
201
+ console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
202
+ return browser2;
203
+ } catch {
215
204
  }
216
- console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
217
- const browser2 = await promiseWithTimeout(
218
- import_puppeteer_core.default.connect({
219
- browserURL: `http://127.0.0.1:${CDP_PORT}`,
220
- defaultViewport: null,
221
- targetFilter
222
- }),
223
- 1e4,
224
- "puppeteer.connect to 127.0.0.1 timed out"
225
- );
226
- console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
227
- return browser2;
228
- } catch (error) {
205
+ }
206
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u7AEF\u53E3 ${CDP_PORT} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
207
+ try {
208
+ await startBrowserInBackground();
209
+ } catch (launchError) {
210
+ const msg = launchError instanceof Error ? launchError.message : String(launchError);
211
+ console.error(import_picocolors.default.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
212
+ throw new Error("Browser launch failed.");
213
+ }
214
+ for (const addr of portAddresses) {
215
+ const isReady = await checkCdpReady(addr, 3);
216
+ if (!isReady) continue;
217
+ const browserURL = addr.replace("/json/version", "");
229
218
  try {
230
- const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
231
- if (!isLocalhostReady) {
232
- throw new Error("localhost CDP port not responding");
233
- }
234
- console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
219
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
235
220
  const browser2 = await promiseWithTimeout(
236
- import_puppeteer_core.default.connect({
237
- browserURL: `http://localhost:${CDP_PORT}`,
238
- defaultViewport: null,
239
- targetFilter
240
- }),
221
+ import_puppeteer_core.default.connect({ browserURL, defaultViewport: null, targetFilter }),
241
222
  1e4,
242
- "puppeteer.connect to localhost timed out"
223
+ `puppeteer.connect to ${browserURL} after launch timed out`
243
224
  );
244
- console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
225
+ console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
245
226
  return browser2;
246
- } catch (error2) {
247
- console.log(import_picocolors.default.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)}`));
248
- try {
249
- await killProcessOnPortIfZombie(CDP_PORT);
250
- await startBrowserInBackground();
251
- try {
252
- const is127Ready = await checkCdpReady(`http://127.0.0.1:${CDP_PORT}/json/version`, 3);
253
- if (!is127Ready) {
254
- throw new Error("127.0.0.1 CDP port not responding after launch");
255
- }
256
- console.log(import_picocolors.default.yellow("connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
257
- const browser2 = await promiseWithTimeout(
258
- import_puppeteer_core.default.connect({
259
- browserURL: `http://127.0.0.1:${CDP_PORT}`,
260
- defaultViewport: null,
261
- targetFilter
262
- }),
263
- 1e4,
264
- "puppeteer.connect to 127.0.0.1 after launch timed out"
265
- );
266
- console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
267
- return browser2;
268
- } catch (e) {
269
- const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
270
- if (!isLocalhostReady) {
271
- throw new Error("localhost CDP port not responding after launch");
272
- }
273
- console.log(import_picocolors.default.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
274
- const browser2 = await promiseWithTimeout(
275
- import_puppeteer_core.default.connect({
276
- browserURL: `http://localhost:${CDP_PORT}`,
277
- defaultViewport: null,
278
- targetFilter
279
- }),
280
- 1e4,
281
- "puppeteer.connect to localhost after launch timed out"
282
- );
283
- console.log(import_picocolors.default.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
284
- return browser2;
285
- }
286
- } catch (launchError) {
287
- const msg = launchError instanceof Error ? launchError.message : String(launchError);
288
- console.error(import_picocolors.default.red(`\u65E0\u6CD5\u8FDE\u63A5\u6216\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
289
- console.error(import_picocolors.default.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`));
290
- console.error(import_picocolors.default.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`));
291
- throw new Error("Browser connection failed.");
292
- }
227
+ } catch {
293
228
  }
294
229
  }
230
+ 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`);
295
231
  }
296
232
  async function getPageTargetId(page2) {
297
233
  const session = await page2.target().createCDPSession();
@@ -369,14 +305,12 @@ async function getTargetPage(browser2, tabid2) {
369
305
  let activeTargetId = null;
370
306
  for (const url of urls) {
371
307
  try {
372
- const res = await fetchWithTimeout(url, 1e3);
373
- if (res.ok) {
374
- const targetsData = await res.json();
375
- const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
376
- if (active) {
377
- activeTargetId = active.id;
378
- break;
379
- }
308
+ const body = await httpGet(url, 1e3);
309
+ const targetsData = JSON.parse(body);
310
+ const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
311
+ if (active) {
312
+ activeTargetId = active.id;
313
+ break;
380
314
  }
381
315
  } catch {
382
316
  }
@@ -422,7 +356,7 @@ async function injectWebMCPPolyfillAndTools(page2, force = false) {
422
356
  const msg = err instanceof Error ? err.message : String(err);
423
357
  throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
424
358
  }
425
- await new Promise((resolve) => setTimeout(resolve, 300));
359
+ await new Promise((resolve2) => setTimeout(resolve2, 300));
426
360
  }
427
361
  await injectDomainTools(page2);
428
362
  }
@@ -510,7 +444,7 @@ async function stateCommand({ tabid: tabid2 }) {
510
444
  if (pUrl.startsWith("devtools://")) return null;
511
445
  const pTitle = await Promise.race([
512
446
  p.title().catch(() => "Unknown"),
513
- new Promise((resolve) => setTimeout(() => resolve("Unknown"), 500))
447
+ new Promise((resolve2) => setTimeout(() => resolve2("Unknown"), 500))
514
448
  ]);
515
449
  return {
516
450
  // 使用真实的 Chrome target ID,而非数组下标
@@ -616,7 +550,7 @@ async function tabsOpenCommand(url) {
616
550
  url = normalizeUrl(url);
617
551
  await page2.goto(url, { waitUntil: "domcontentloaded" });
618
552
  await injectIntoPage(page2);
619
- await new Promise((resolve) => setTimeout(resolve, 500));
553
+ await new Promise((resolve2) => setTimeout(resolve2, 500));
620
554
  return {
621
555
  success: true,
622
556
  tabid: await getPageTargetId(page2),
@@ -717,7 +651,7 @@ async function tabsForwardCommand(tabid2) {
717
651
  // package.json
718
652
  var package_default = {
719
653
  name: "@opentiny/webmcp-cli",
720
- version: "0.0.1-alpha.3",
654
+ version: "0.0.1-alpha.4",
721
655
  type: "module",
722
656
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
723
657
  main: "dist/index.js",
@@ -779,6 +713,19 @@ function handleCommandError(error, commandName) {
779
713
  console.error(import_picocolors2.default.red(`Error executing ${commandName} command: ${msg}`));
780
714
  process.exit(1);
781
715
  }
716
+ function expandFileRefs(argsJson) {
717
+ return argsJson.replace(/"@(base64)?file:([^"]+)"/g, (_match, base64Flag, filePath) => {
718
+ const absPath = (0, import_path2.resolve)(process.cwd(), filePath);
719
+ let content;
720
+ try {
721
+ content = (0, import_fs2.readFileSync)(absPath, "utf-8");
722
+ } catch (e) {
723
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}": ${e.message}`);
724
+ }
725
+ const value = base64Flag ? Buffer.from(content, "utf-8").toString("base64") : content;
726
+ return JSON.stringify(value);
727
+ });
728
+ }
782
729
  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) => {
783
730
  const opts = thisCommand.opts();
784
731
  if (opts.workspace) {
@@ -793,11 +740,31 @@ program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D
793
740
  handleCommandError(error, "state");
794
741
  }
795
742
  });
796
- 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) => {
743
+ 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(
744
+ "-f, --file <path>",
745
+ `\u4ECE\u6307\u5B9A .json \u6587\u4EF6\u8BFB\u53D6\u6574\u4E2A argsJson\uFF08\u6587\u4EF6\u5185\u5BB9\u5373\u4E3A\u53C2\u6570 JSON\uFF09\u3002
746
+ \u5982\u9700\u5728 argsJson \u4E2D\u5185\u8054\u5F15\u7528\u6587\u4EF6\uFF0C\u4F7F\u7528\u5360\u4F4D\u7B26\u8BED\u6CD5\uFF1A
747
+ @file:<path> \u8BFB\u53D6\u6587\u4EF6\u539F\u59CB\u6587\u672C
748
+ @base64file:<path> \u8BFB\u53D6\u6587\u4EF6\u5E76 Base64 \u7F16\u7801
749
+ \u793A\u4F8B\uFF1Awebmcp-cli run mytool '{"content":"@base64file:./doc.md"}'`
750
+ ).action(async (toolName, argsJson, options) => {
797
751
  try {
752
+ let finalArgsJson = argsJson;
753
+ if (options.file) {
754
+ const filePath = (0, import_path2.resolve)(process.cwd(), options.file);
755
+ try {
756
+ finalArgsJson = (0, import_fs2.readFileSync)(filePath, "utf-8").trim();
757
+ } catch (e) {
758
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6 "${filePath}": ${e.message}`);
759
+ }
760
+ }
761
+ if (!finalArgsJson) {
762
+ throw new Error("\u5FC5\u987B\u63D0\u4F9B argsJson \u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
763
+ }
764
+ finalArgsJson = expandFileRefs(finalArgsJson);
798
765
  const result2 = await runCommand({
799
766
  toolName,
800
- argsJson,
767
+ argsJson: finalArgsJson,
801
768
  tabid: parseTabId(options.tabid)
802
769
  });
803
770
  console.log(JSON.stringify(result2, null, 2));