@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.js CHANGED
@@ -161,21 +161,28 @@ ${hint}`);
161
161
  }
162
162
  }
163
163
  function prepareRunArgsJson(args, fileContent, baseDir) {
164
- let raw = "";
165
164
  if (fileContent !== void 0) {
166
- raw = fileContent.trim();
167
- } else if (args.length > 0) {
168
- const joined = joinShellArgs(args);
169
- if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
170
- raw = joined;
171
- } else if (args.every((arg) => arg.includes("="))) {
172
- raw = buildArgsFromKeyValuePairs(args);
173
- } else {
174
- raw = joined;
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");
175
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;
176
183
  }
177
- if (!raw) {
178
- throw new Error("\u5FC5\u987B\u63D0\u4F9B\u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
184
+ if (!raw.trim()) {
185
+ throw new Error("\u672A\u63D0\u4F9B\u6709\u6548\u53C2\u6570");
179
186
  }
180
187
  const expanded = expandFileRefs(raw, baseDir);
181
188
  return parseArgsJson(expanded);
@@ -194,6 +201,30 @@ var __filename = fileURLToPath2(import.meta.url);
194
201
  var __dirname = path.dirname(__filename);
195
202
  var CDP_PORT = 9222;
196
203
  var CDP_URL = `http://localhost:${CDP_PORT}`;
204
+ function getWorkspaceDir() {
205
+ return process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
206
+ }
207
+ function getLastTabIdFilePath() {
208
+ return path.join(getWorkspaceDir(), ".last-tab-id");
209
+ }
210
+ function setLastActiveTabId(tabid) {
211
+ try {
212
+ const dir = getWorkspaceDir();
213
+ if (!fs.existsSync(dir)) {
214
+ fs.mkdirSync(dir, { recursive: true });
215
+ }
216
+ fs.writeFileSync(getLastTabIdFilePath(), tabid, "utf-8");
217
+ } catch {
218
+ }
219
+ }
220
+ function getLastActiveTabId() {
221
+ try {
222
+ const tabid = fs.readFileSync(getLastTabIdFilePath(), "utf-8").trim();
223
+ return tabid || null;
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
197
228
  function httpGet(url, timeoutMs = 1500) {
198
229
  return new Promise((resolve3, reject) => {
199
230
  const req = http.get(url, { timeout: timeoutMs }, (res) => {
@@ -240,6 +271,31 @@ async function checkCdpReady(url, retries = 3) {
240
271
  }
241
272
  return false;
242
273
  }
274
+ async function findAvailableCdpUrl(addresses, retries = 2) {
275
+ for (const addr of addresses) {
276
+ if (await checkCdpReady(addr, retries)) {
277
+ return addr.replace("/json/version", "");
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
283
+ try {
284
+ return await promiseWithTimeout(
285
+ puppeteer.connect({ browserURL: browserBaseUrl, defaultViewport: null }),
286
+ 1e4,
287
+ `puppeteer.connect to ${browserBaseUrl} timed out`
288
+ );
289
+ } catch (err) {
290
+ if (attempt >= maxAttempts) {
291
+ throw err;
292
+ }
293
+ const delayMs = 500 * attempt;
294
+ console.log(pc.yellow(`connectBrowser: \u7B2C ${attempt} \u6B21\u8FDE\u63A5\u5931\u8D25\uFF0C${delayMs}ms \u540E\u91CD\u8BD5...`));
295
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
296
+ return connectPuppeteer(browserBaseUrl, attempt + 1, maxAttempts);
297
+ }
298
+ }
243
299
  function getDefaultBrowserPath() {
244
300
  const platform = os.platform();
245
301
  if (platform === "darwin") {
@@ -286,72 +342,56 @@ async function startBrowserInBackground() {
286
342
  }
287
343
  console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
288
344
  const userDataDir = process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
345
+ const launchArgs = [
346
+ `--remote-debugging-port=${CDP_PORT}`,
347
+ `--user-data-dir=${userDataDir}`,
348
+ "--no-first-run",
349
+ "--no-default-browser-check"
350
+ ];
351
+ if (os.platform() === "win32") {
352
+ launchArgs.push("--remote-debugging-address=127.0.0.1");
353
+ }
289
354
  const child = spawn(
290
355
  browserInfo.path,
291
- [
292
- `--remote-debugging-port=${CDP_PORT}`,
293
- `--user-data-dir=${userDataDir}`,
294
- "--no-first-run",
295
- "--no-default-browser-check"
296
- ],
356
+ launchArgs,
297
357
  {
298
358
  detached: true,
299
359
  stdio: "ignore"
300
360
  }
301
361
  );
302
362
  child.unref();
303
- for (let i = 0; i < 20; i++) {
304
- try {
305
- const urls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
306
- for (const url of urls) {
307
- try {
308
- await httpGet(url, 1e3);
309
- console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
310
- return;
311
- } catch (err) {
312
- }
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 {
313
372
  }
314
- } catch (e) {
315
373
  }
316
374
  await new Promise((resolve3) => setTimeout(resolve3, 500));
317
375
  }
318
376
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
319
377
  }
320
378
  async function connectBrowser() {
321
- const targetFilter = (target) => {
322
- try {
323
- const info = typeof target._getTargetInfo === "function" ? target._getTargetInfo() : target;
324
- const type = info.type || "";
325
- const url = info.url || "";
326
- if (type === "service_worker" || type === "shared_worker" || type === "iframe" || type === "other" || type === "webview" || type === "background_page") {
327
- return false;
328
- }
329
- if (url.startsWith("devtools://") || url.startsWith("chrome-extension://")) {
330
- return false;
331
- }
332
- return true;
333
- } catch (e) {
334
- return false;
335
- }
336
- };
337
- const portAddresses = [
379
+ const versionAddresses = [
338
380
  `http://127.0.0.1:${CDP_PORT}/json/version`,
339
381
  `http://localhost:${CDP_PORT}/json/version`
340
382
  ];
341
- for (const addr of portAddresses) {
342
- const isReady = await checkCdpReady(addr, 2);
343
- if (!isReady) continue;
344
- const browserURL = addr.replace("/json/version", "");
383
+ const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
384
+ if (existingUrl) {
385
+ console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
345
386
  try {
346
- console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
347
- const browser = await promiseWithTimeout(
348
- puppeteer.connect({ browserURL, defaultViewport: null, targetFilter }),
349
- 1e4,
350
- `puppeteer.connect to ${browserURL} timed out`
351
- );
352
- console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
387
+ const browser = await connectPuppeteer(existingUrl);
388
+ console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
353
389
  return browser;
354
- } catch {
390
+ } catch (err) {
391
+ const msg = err instanceof Error ? err.message : String(err);
392
+ throw new Error(
393
+ `\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`
394
+ );
355
395
  }
356
396
  }
357
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...`));
@@ -362,23 +402,18 @@ async function connectBrowser() {
362
402
  console.error(pc.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
363
403
  throw new Error("Browser launch failed.");
364
404
  }
365
- for (const addr of portAddresses) {
366
- const isReady = await checkCdpReady(addr, 3);
367
- if (!isReady) continue;
368
- const browserURL = addr.replace("/json/version", "");
369
- try {
370
- console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${browserURL}...`));
371
- const browser = await promiseWithTimeout(
372
- puppeteer.connect({ browserURL, defaultViewport: null, targetFilter }),
373
- 1e4,
374
- `puppeteer.connect to ${browserURL} after launch timed out`
375
- );
376
- console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${browserURL}`));
377
- return browser;
378
- } catch {
379
- }
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
+ try {
410
+ console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
411
+ const browser = await connectPuppeteer(launchedUrl);
412
+ console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
413
+ return browser;
414
+ } 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`);
380
416
  }
381
- 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`);
382
417
  }
383
418
  async function getPageTargetId(page) {
384
419
  const session = await page.target().createCDPSession();
@@ -448,33 +483,42 @@ async function getTargetPage(browser, tabid) {
448
483
  }
449
484
  targetPage = await target.page();
450
485
  } else {
451
- try {
452
- const urls = [
453
- `http://localhost:${CDP_PORT}/json/list`,
454
- `http://127.0.0.1:${CDP_PORT}/json/list`
455
- ];
456
- let activeTargetId = null;
457
- for (const url of urls) {
458
- try {
459
- const body = await httpGet(url, 1e3);
460
- const targetsData = JSON.parse(body);
461
- const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
462
- if (active) {
463
- activeTargetId = active.id;
464
- break;
486
+ const lastTabId = getLastActiveTabId();
487
+ if (lastTabId) {
488
+ const lastTarget = findPageTargetByTabId(browser, lastTabId);
489
+ if (lastTarget) {
490
+ targetPage = await lastTarget.page();
491
+ }
492
+ }
493
+ if (!targetPage) {
494
+ try {
495
+ const urls = [
496
+ `http://localhost:${CDP_PORT}/json/list`,
497
+ `http://127.0.0.1:${CDP_PORT}/json/list`
498
+ ];
499
+ let activeTargetId = null;
500
+ for (const url of urls) {
501
+ try {
502
+ const body = await httpGet(url, 1e3);
503
+ const targetsData = JSON.parse(body);
504
+ const active = targetsData.find((t) => t.type === "page" && !t.url.startsWith("devtools://"));
505
+ if (active) {
506
+ activeTargetId = active.id;
507
+ break;
508
+ }
509
+ } catch {
465
510
  }
466
- } catch {
467
511
  }
468
- }
469
- if (activeTargetId) {
470
- for (const target of pageTargets) {
471
- if (getTargetIdFromTarget(target) === activeTargetId) {
472
- targetPage = await target.page();
473
- break;
512
+ if (activeTargetId) {
513
+ for (const target of pageTargets) {
514
+ if (getTargetIdFromTarget(target) === activeTargetId) {
515
+ targetPage = await target.page();
516
+ break;
517
+ }
474
518
  }
475
519
  }
520
+ } catch {
476
521
  }
477
- } catch {
478
522
  }
479
523
  if (!targetPage) {
480
524
  const lastTarget = pageTargets[pageTargets.length - 1];
@@ -555,6 +599,12 @@ async function injectDomainTools(page) {
555
599
  if (!fs.existsSync(toolBundlePath)) {
556
600
  return;
557
601
  }
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;
607
+ }
558
608
  console.log(pc.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
559
609
  const toolScript = fs.readFileSync(toolBundlePath, "utf-8");
560
610
  try {
@@ -583,6 +633,7 @@ async function stateCommand({ tabid }) {
583
633
  }
584
634
  return { url, title, webmcpTools };
585
635
  });
636
+ const activeTabid = await getPageTargetId(page).catch(() => null);
586
637
  const pages = await browser.pages();
587
638
  const tabs2 = await Promise.all(
588
639
  pages.map(async (p) => {
@@ -602,6 +653,7 @@ async function stateCommand({ tabid }) {
602
653
  );
603
654
  return {
604
655
  ...state,
656
+ activeTabid,
605
657
  tabs: tabs2.filter(Boolean)
606
658
  };
607
659
  } finally {
@@ -618,28 +670,51 @@ async function runCommand({
618
670
  const browser = await connectBrowser();
619
671
  try {
620
672
  const page = await getTargetPage(browser, tabid);
673
+ const urlBefore = page.url();
621
674
  const cleanedArgs = argsJson.trim();
622
- const result = await page.evaluate(async (name, inputString) => {
623
- const mcp = navigator.modelContextTesting || navigator.modelContext;
624
- if (!mcp || typeof mcp.executeTool !== "function") {
625
- throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
626
- }
627
- let res = await mcp.executeTool(name, inputString);
628
- if (typeof res === "string") {
675
+ let result;
676
+ try {
677
+ result = await page.evaluate(async (name, inputString) => {
678
+ const mcp = navigator.modelContextTesting || navigator.modelContext;
679
+ if (!mcp || typeof mcp.executeTool !== "function") {
680
+ throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
681
+ }
682
+ let res = await mcp.executeTool(name, inputString);
683
+ if (typeof res === "string") {
684
+ try {
685
+ res = JSON.parse(res);
686
+ } catch {
687
+ }
688
+ }
689
+ if (res === void 0 || res === null) {
690
+ throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u5B9E\u73B0\u662F\u5426\u7F3A\u5C11 return");
691
+ }
692
+ if (typeof res === "object" && res.success === false) {
693
+ const failed = res;
694
+ throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
695
+ }
696
+ return res;
697
+ }, toolName, cleanedArgs);
698
+ } catch (evalError) {
699
+ const errMsg = evalError instanceof Error ? evalError.message : String(evalError);
700
+ const isContextDestroyed = errMsg.includes("context was destroyed") || errMsg.includes("Execution context was destroyed") || errMsg.includes("Cannot read properties of null") && errMsg.includes("context");
701
+ if (isContextDestroyed) {
702
+ await new Promise((resolve3) => setTimeout(resolve3, 800));
703
+ let newUrl = "";
629
704
  try {
630
- res = JSON.parse(res);
705
+ newUrl = page.url();
631
706
  } catch {
632
707
  }
708
+ if (newUrl && newUrl !== urlBefore) {
709
+ return {
710
+ success: true,
711
+ message: `\u5DE5\u5177 ${toolName} \u6267\u884C\u5B8C\u6210\uFF0C\u9875\u9762\u5DF2\u5BFC\u822A\u81F3 ${newUrl}`,
712
+ navigatedTo: newUrl
713
+ };
714
+ }
633
715
  }
634
- if (res === void 0 || res === null) {
635
- throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u5B9E\u73B0\u662F\u5426\u7F3A\u5C11 return");
636
- }
637
- if (typeof res === "object" && res.success === false) {
638
- const failed = res;
639
- throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
640
- }
641
- return res;
642
- }, toolName, cleanedArgs);
716
+ throw evalError;
717
+ }
643
718
  return result;
644
719
  } finally {
645
720
  await browser.disconnect();
@@ -671,11 +746,19 @@ async function tabsOpenCommand(url) {
671
746
  await page.goto(url, { waitUntil: "domcontentloaded" });
672
747
  await injectIntoPage(page);
673
748
  await new Promise((resolve3) => setTimeout(resolve3, 500));
749
+ let tabid;
750
+ try {
751
+ tabid = await getPageTargetId(page);
752
+ await activateTabById(browser, tabid);
753
+ setLastActiveTabId(tabid);
754
+ } catch (e) {
755
+ console.warn("Best-effort tab activation failed:", e);
756
+ }
674
757
  return {
675
758
  success: true,
676
- tabid: await getPageTargetId(page),
759
+ tabid,
677
760
  url: page.url(),
678
- title: await page.title()
761
+ title: await page.title().catch(() => "")
679
762
  };
680
763
  } finally {
681
764
  await browser.disconnect();
@@ -707,9 +790,11 @@ async function tabsSwitchCommand(tabid) {
707
790
  try {
708
791
  await activateTabById(browser, tabid);
709
792
  const page = await getTargetPage(browser, tabid);
793
+ const realTabid = await getPageTargetId(page);
794
+ setLastActiveTabId(realTabid);
710
795
  return {
711
796
  success: true,
712
- tabid: await getPageTargetId(page),
797
+ tabid: realTabid,
713
798
  url: page.url(),
714
799
  title: await page.title()
715
800
  };
@@ -771,7 +856,7 @@ async function tabsForwardCommand(tabid) {
771
856
  // package.json
772
857
  var package_default = {
773
858
  name: "@opentiny/webmcp-cli",
774
- version: "0.0.2",
859
+ version: "0.0.4",
775
860
  type: "module",
776
861
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
777
862
  main: "dist/index.js",