@opentiny/webmcp-cli 0.0.2 → 0.0.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
@@ -184,21 +184,28 @@ ${hint}`);
184
184
  }
185
185
  }
186
186
  function prepareRunArgsJson(args, fileContent, baseDir) {
187
- let raw = "";
188
187
  if (fileContent !== void 0) {
189
- raw = fileContent.trim();
190
- } else if (args.length > 0) {
191
- const joined = joinShellArgs(args);
192
- if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
193
- raw = joined;
194
- } else if (args.every((arg) => arg.includes("="))) {
195
- raw = buildArgsFromKeyValuePairs(args);
196
- } else {
197
- raw = joined;
188
+ const trimmed = fileContent.trim();
189
+ if (!trimmed) {
190
+ throw new Error("\u901A\u8FC7 -f/--file \u6307\u5B9A\u7684\u53C2\u6570\u6587\u4EF6\u5185\u5BB9\u4E0D\u80FD\u4E3A\u7A7A");
198
191
  }
192
+ const expanded2 = expandFileRefs(trimmed, baseDir);
193
+ return parseArgsJson(expanded2);
194
+ }
195
+ if (args.length === 0) {
196
+ return "{}";
197
+ }
198
+ const joined = joinShellArgs(args);
199
+ let raw = "";
200
+ if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
201
+ raw = joined;
202
+ } else if (args.every((arg) => arg.includes("="))) {
203
+ raw = buildArgsFromKeyValuePairs(args);
204
+ } else {
205
+ raw = joined;
199
206
  }
200
- if (!raw) {
201
- throw new Error("\u5FC5\u987B\u63D0\u4F9B\u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
207
+ if (!raw.trim()) {
208
+ throw new Error("\u672A\u63D0\u4F9B\u6709\u6548\u53C2\u6570");
202
209
  }
203
210
  const expanded = expandFileRefs(raw, baseDir);
204
211
  return parseArgsJson(expanded);
@@ -218,6 +225,30 @@ var __filename = (0, import_url2.fileURLToPath)(import_meta.url);
218
225
  var __dirname = import_path2.default.dirname(__filename);
219
226
  var CDP_PORT = 9222;
220
227
  var CDP_URL = `http://localhost:${CDP_PORT}`;
228
+ function getWorkspaceDir() {
229
+ return process.env.WEBMCP_WORKSPACE || import_path2.default.join(import_os.default.homedir(), ".webmcp_chrome_profile");
230
+ }
231
+ function getLastTabIdFilePath() {
232
+ return import_path2.default.join(getWorkspaceDir(), ".last-tab-id");
233
+ }
234
+ function setLastActiveTabId(tabid) {
235
+ try {
236
+ const dir = getWorkspaceDir();
237
+ if (!import_fs2.default.existsSync(dir)) {
238
+ import_fs2.default.mkdirSync(dir, { recursive: true });
239
+ }
240
+ import_fs2.default.writeFileSync(getLastTabIdFilePath(), tabid, "utf-8");
241
+ } catch {
242
+ }
243
+ }
244
+ function getLastActiveTabId() {
245
+ try {
246
+ const tabid = import_fs2.default.readFileSync(getLastTabIdFilePath(), "utf-8").trim();
247
+ return tabid || null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
221
252
  function httpGet(url, timeoutMs = 1500) {
222
253
  return new Promise((resolve3, reject) => {
223
254
  const req = import_http.default.get(url, { timeout: timeoutMs }, (res) => {
@@ -264,6 +295,31 @@ async function checkCdpReady(url, retries = 3) {
264
295
  }
265
296
  return false;
266
297
  }
298
+ async function findAvailableCdpUrl(addresses, retries = 2) {
299
+ for (const addr of addresses) {
300
+ if (await checkCdpReady(addr, retries)) {
301
+ return addr.replace("/json/version", "");
302
+ }
303
+ }
304
+ return null;
305
+ }
306
+ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
307
+ try {
308
+ return await promiseWithTimeout(
309
+ import_puppeteer_core.default.connect({ browserURL: browserBaseUrl, defaultViewport: null }),
310
+ 1e4,
311
+ `puppeteer.connect to ${browserBaseUrl} timed out`
312
+ );
313
+ } catch (err) {
314
+ if (attempt >= maxAttempts) {
315
+ throw err;
316
+ }
317
+ const delayMs = 500 * attempt;
318
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u7B2C ${attempt} \u6B21\u8FDE\u63A5\u5931\u8D25\uFF0C${delayMs}ms \u540E\u91CD\u8BD5...`));
319
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
320
+ return connectPuppeteer(browserBaseUrl, attempt + 1, maxAttempts);
321
+ }
322
+ }
267
323
  function getDefaultBrowserPath() {
268
324
  const platform = import_os.default.platform();
269
325
  if (platform === "darwin") {
@@ -310,72 +366,56 @@ async function startBrowserInBackground() {
310
366
  }
311
367
  console.log(import_picocolors.default.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
312
368
  const userDataDir = process.env.WEBMCP_WORKSPACE || import_path2.default.join(import_os.default.homedir(), ".webmcp_chrome_profile");
369
+ const launchArgs = [
370
+ `--remote-debugging-port=${CDP_PORT}`,
371
+ `--user-data-dir=${userDataDir}`,
372
+ "--no-first-run",
373
+ "--no-default-browser-check"
374
+ ];
375
+ if (import_os.default.platform() === "win32") {
376
+ launchArgs.push("--remote-debugging-address=127.0.0.1");
377
+ }
313
378
  const child = (0, import_child_process.spawn)(
314
379
  browserInfo.path,
315
- [
316
- `--remote-debugging-port=${CDP_PORT}`,
317
- `--user-data-dir=${userDataDir}`,
318
- "--no-first-run",
319
- "--no-default-browser-check"
320
- ],
380
+ launchArgs,
321
381
  {
322
382
  detached: true,
323
383
  stdio: "ignore"
324
384
  }
325
385
  );
326
386
  child.unref();
327
- for (let i = 0; i < 20; i++) {
328
- try {
329
- const urls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
330
- for (const url of urls) {
331
- try {
332
- await httpGet(url, 1e3);
333
- console.log(import_picocolors.default.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
334
- return;
335
- } catch (err) {
336
- }
387
+ const pollUrls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
388
+ for (let i = 0; i < 30; i++) {
389
+ for (const url of pollUrls) {
390
+ try {
391
+ await httpGet(url, 1e3);
392
+ console.log(import_picocolors.default.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
393
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
394
+ return;
395
+ } catch {
337
396
  }
338
- } catch (e) {
339
397
  }
340
398
  await new Promise((resolve3) => setTimeout(resolve3, 500));
341
399
  }
342
400
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
343
401
  }
344
402
  async function connectBrowser() {
345
- const targetFilter = (target) => {
346
- try {
347
- const info = typeof target._getTargetInfo === "function" ? target._getTargetInfo() : target;
348
- const type = info.type || "";
349
- const url = info.url || "";
350
- if (type === "service_worker" || type === "shared_worker" || type === "iframe" || type === "other" || type === "webview" || type === "background_page") {
351
- return false;
352
- }
353
- if (url.startsWith("devtools://") || url.startsWith("chrome-extension://")) {
354
- return false;
355
- }
356
- return true;
357
- } catch (e) {
358
- return false;
359
- }
360
- };
361
- const portAddresses = [
403
+ const versionAddresses = [
362
404
  `http://127.0.0.1:${CDP_PORT}/json/version`,
363
405
  `http://localhost:${CDP_PORT}/json/version`
364
406
  ];
365
- for (const addr of portAddresses) {
366
- const isReady = await checkCdpReady(addr, 2);
367
- if (!isReady) continue;
368
- const browserURL = addr.replace("/json/version", "");
407
+ const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
408
+ if (existingUrl) {
409
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
369
410
  try {
370
- console.log(import_picocolors.default.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
371
- const browser = await promiseWithTimeout(
372
- import_puppeteer_core.default.connect({ browserURL, defaultViewport: null, targetFilter }),
373
- 1e4,
374
- `puppeteer.connect to ${browserURL} timed out`
375
- );
376
- console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
411
+ const browser = await connectPuppeteer(existingUrl);
412
+ console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
377
413
  return browser;
378
- } catch {
414
+ } catch (err) {
415
+ const msg = err instanceof Error ? err.message : String(err);
416
+ throw new Error(
417
+ `\u65E0\u6CD5\u8FDE\u63A5\u5230\u5DF2\u5728\u8FD0\u884C\u7684\u6D4F\u89C8\u5668\uFF08${existingUrl}\uFF09\uFF1A${msg}\u3002CDP \u7AEF\u53E3\u53EF\u7528\uFF0C\u8BF7\u52FF\u91CD\u590D\u542F\u52A8\u6D4F\u89C8\u5668\uFF1B\u53EF\u5173\u95ED\u591A\u4F59\u7A97\u53E3\u540E\u91CD\u8BD5\u3002`
418
+ );
379
419
  }
380
420
  }
381
421
  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...`));
@@ -386,23 +426,18 @@ async function connectBrowser() {
386
426
  console.error(import_picocolors.default.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
387
427
  throw new Error("Browser launch failed.");
388
428
  }
389
- for (const addr of portAddresses) {
390
- const isReady = await checkCdpReady(addr, 3);
391
- if (!isReady) continue;
392
- const browserURL = addr.replace("/json/version", "");
393
- try {
394
- console.log(import_picocolors.default.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
395
- const browser = await promiseWithTimeout(
396
- import_puppeteer_core.default.connect({ browserURL, defaultViewport: null, targetFilter }),
397
- 1e4,
398
- `puppeteer.connect to ${browserURL} after launch timed out`
399
- );
400
- console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
401
- return browser;
402
- } catch {
403
- }
429
+ const launchedUrl = await findAvailableCdpUrl(versionAddresses, 5);
430
+ if (!launchedUrl) {
431
+ 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`);
432
+ }
433
+ try {
434
+ console.log(import_picocolors.default.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
435
+ const browser = await connectPuppeteer(launchedUrl);
436
+ console.log(import_picocolors.default.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
437
+ return browser;
438
+ } catch {
439
+ 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`);
404
440
  }
405
- 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`);
406
441
  }
407
442
  async function getPageTargetId(page) {
408
443
  const session = await page.target().createCDPSession();
@@ -472,33 +507,42 @@ async function getTargetPage(browser, tabid) {
472
507
  }
473
508
  targetPage = await target.page();
474
509
  } else {
475
- try {
476
- const urls = [
477
- `http://localhost:${CDP_PORT}/json/list`,
478
- `http://127.0.0.1:${CDP_PORT}/json/list`
479
- ];
480
- let activeTargetId = null;
481
- for (const url of urls) {
482
- try {
483
- const body = await httpGet(url, 1e3);
484
- const targetsData = JSON.parse(body);
485
- const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
486
- if (active) {
487
- activeTargetId = active.id;
488
- break;
510
+ const lastTabId = getLastActiveTabId();
511
+ if (lastTabId) {
512
+ const lastTarget = findPageTargetByTabId(browser, lastTabId);
513
+ if (lastTarget) {
514
+ targetPage = await lastTarget.page();
515
+ }
516
+ }
517
+ if (!targetPage) {
518
+ try {
519
+ const urls = [
520
+ `http://localhost:${CDP_PORT}/json/list`,
521
+ `http://127.0.0.1:${CDP_PORT}/json/list`
522
+ ];
523
+ let activeTargetId = null;
524
+ for (const url of urls) {
525
+ try {
526
+ const body = await httpGet(url, 1e3);
527
+ const targetsData = JSON.parse(body);
528
+ const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
529
+ if (active) {
530
+ activeTargetId = active.id;
531
+ break;
532
+ }
533
+ } catch {
489
534
  }
490
- } catch {
491
535
  }
492
- }
493
- if (activeTargetId) {
494
- for (const target of pageTargets) {
495
- if (getTargetIdFromTarget(target) === activeTargetId) {
496
- targetPage = await target.page();
497
- break;
536
+ if (activeTargetId) {
537
+ for (const target of pageTargets) {
538
+ if (getTargetIdFromTarget(target) === activeTargetId) {
539
+ targetPage = await target.page();
540
+ break;
541
+ }
498
542
  }
499
543
  }
544
+ } catch {
500
545
  }
501
- } catch {
502
546
  }
503
547
  if (!targetPage) {
504
548
  const lastTarget = pageTargets[pageTargets.length - 1];
@@ -579,6 +623,12 @@ async function injectDomainTools(page) {
579
623
  if (!import_fs2.default.existsSync(toolBundlePath)) {
580
624
  return;
581
625
  }
626
+ const bundleKey = bundleName.replace(/^webmcp-tools\//, "").replace(/\.js$/, "");
627
+ const flagKey = `__webmcptools_${bundleKey.replace(/[^a-zA-Z0-9]/g, "")}`;
628
+ const alreadyInjected = await page.evaluate((key) => !!window[key], flagKey).catch(() => false);
629
+ if (alreadyInjected) {
630
+ return;
631
+ }
582
632
  console.log(import_picocolors.default.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
583
633
  const toolScript = import_fs2.default.readFileSync(toolBundlePath, "utf-8");
584
634
  try {
@@ -607,6 +657,7 @@ async function stateCommand({ tabid }) {
607
657
  }
608
658
  return { url, title, webmcpTools };
609
659
  });
660
+ const activeTabid = await getPageTargetId(page).catch(() => null);
610
661
  const pages = await browser.pages();
611
662
  const tabs2 = await Promise.all(
612
663
  pages.map(async (p) => {
@@ -626,6 +677,7 @@ async function stateCommand({ tabid }) {
626
677
  );
627
678
  return {
628
679
  ...state,
680
+ activeTabid,
629
681
  tabs: tabs2.filter(Boolean)
630
682
  };
631
683
  } finally {
@@ -642,28 +694,51 @@ async function runCommand({
642
694
  const browser = await connectBrowser();
643
695
  try {
644
696
  const page = await getTargetPage(browser, tabid);
697
+ const urlBefore = page.url();
645
698
  const cleanedArgs = argsJson.trim();
646
- const result = await page.evaluate(async (name, inputString) => {
647
- const mcp = navigator.modelContextTesting || navigator.modelContext;
648
- if (!mcp || typeof mcp.executeTool !== "function") {
649
- throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
650
- }
651
- let res = await mcp.executeTool(name, inputString);
652
- if (typeof res === "string") {
699
+ let result;
700
+ try {
701
+ result = await page.evaluate(async (name, inputString) => {
702
+ const mcp = navigator.modelContextTesting || navigator.modelContext;
703
+ if (!mcp || typeof mcp.executeTool !== "function") {
704
+ throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
705
+ }
706
+ let res = await mcp.executeTool(name, inputString);
707
+ if (typeof res === "string") {
708
+ try {
709
+ res = JSON.parse(res);
710
+ } catch {
711
+ }
712
+ }
713
+ if (res === void 0 || res === null) {
714
+ throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u5B9E\u73B0\u662F\u5426\u7F3A\u5C11 return");
715
+ }
716
+ if (typeof res === "object" && res.success === false) {
717
+ const failed = res;
718
+ throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
719
+ }
720
+ return res;
721
+ }, toolName, cleanedArgs);
722
+ } catch (evalError) {
723
+ const errMsg = evalError instanceof Error ? evalError.message : String(evalError);
724
+ const isContextDestroyed = errMsg.includes("context was destroyed") || errMsg.includes("Execution context was destroyed") || errMsg.includes("Cannot read properties of null") && errMsg.includes("context");
725
+ if (isContextDestroyed) {
726
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
727
+ let newUrl = "";
653
728
  try {
654
- res = JSON.parse(res);
729
+ newUrl = page.url();
655
730
  } catch {
656
731
  }
732
+ if (newUrl && newUrl !== urlBefore) {
733
+ return {
734
+ success: true,
735
+ message: `\u5DE5\u5177 ${toolName} \u6267\u884C\u5B8C\u6210\uFF0C\u9875\u9762\u5DF2\u5BFC\u822A\u81F3 ${newUrl}`,
736
+ navigatedTo: newUrl
737
+ };
738
+ }
657
739
  }
658
- if (res === void 0 || res === null) {
659
- throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u5B9E\u73B0\u662F\u5426\u7F3A\u5C11 return");
660
- }
661
- if (typeof res === "object" && res.success === false) {
662
- const failed = res;
663
- throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
664
- }
665
- return res;
666
- }, toolName, cleanedArgs);
740
+ throw evalError;
741
+ }
667
742
  return result;
668
743
  } finally {
669
744
  await browser.disconnect();
@@ -695,11 +770,19 @@ async function tabsOpenCommand(url) {
695
770
  await page.goto(url, { waitUntil: "domcontentloaded" });
696
771
  await injectIntoPage(page);
697
772
  await new Promise((resolve3) => setTimeout(resolve3, 500));
773
+ let tabid;
774
+ try {
775
+ tabid = await getPageTargetId(page);
776
+ await activateTabById(browser, tabid);
777
+ setLastActiveTabId(tabid);
778
+ } catch (e) {
779
+ console.warn("Best-effort tab activation failed:", e);
780
+ }
698
781
  return {
699
782
  success: true,
700
- tabid: await getPageTargetId(page),
783
+ tabid,
701
784
  url: page.url(),
702
- title: await page.title()
785
+ title: await page.title().catch(() => "")
703
786
  };
704
787
  } finally {
705
788
  await browser.disconnect();
@@ -731,9 +814,11 @@ async function tabsSwitchCommand(tabid) {
731
814
  try {
732
815
  await activateTabById(browser, tabid);
733
816
  const page = await getTargetPage(browser, tabid);
817
+ const realTabid = await getPageTargetId(page);
818
+ setLastActiveTabId(realTabid);
734
819
  return {
735
820
  success: true,
736
- tabid: await getPageTargetId(page),
821
+ tabid: realTabid,
737
822
  url: page.url(),
738
823
  title: await page.title()
739
824
  };
@@ -795,7 +880,7 @@ async function tabsForwardCommand(tabid) {
795
880
  // package.json
796
881
  var package_default = {
797
882
  name: "@opentiny/webmcp-cli",
798
- version: "0.0.2",
883
+ version: "0.0.4",
799
884
  type: "module",
800
885
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
801
886
  main: "dist/index.js",