@opentiny/webmcp-cli 0.0.4 → 0.0.6

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
@@ -3,7 +3,7 @@
3
3
  // src/bin.ts
4
4
  import fs2 from "fs";
5
5
  import path2 from "path";
6
- import os2 from "os";
6
+ import os3 from "os";
7
7
  import { Command } from "commander";
8
8
  import pc2 from "picocolors";
9
9
  import { readFileSync as readFileSync2 } from "fs";
@@ -625,10 +625,10 @@ async function stateCommand({ tabid }) {
625
625
  const state = await page.evaluate(async () => {
626
626
  const url = document.URL;
627
627
  const title = document.title;
628
- const mcp = navigator.modelContextTesting || navigator.modelContext;
628
+ const mcp = document.modelContext;
629
629
  let webmcpTools = [];
630
630
  if (mcp && typeof mcp.listTools === "function") {
631
- const toolsResult = await mcp.listTools();
631
+ const toolsResult = await mcp.getTools();
632
632
  webmcpTools = toolsResult?.tools || toolsResult || [];
633
633
  }
634
634
  return { url, title, webmcpTools };
@@ -661,6 +661,155 @@ async function stateCommand({ tabid }) {
661
661
  }
662
662
  }
663
663
 
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
+
664
813
  // src/commands/run.ts
665
814
  async function runCommand({
666
815
  toolName,
@@ -672,14 +821,28 @@ async function runCommand({
672
821
  const page = await getTargetPage(browser, tabid);
673
822
  const urlBefore = page.url();
674
823
  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
+ }
675
835
  let result;
676
836
  try {
677
837
  result = await page.evaluate(async (name, inputString) => {
678
- const mcp = navigator.modelContextTesting || navigator.modelContext;
838
+ const mcp = document.modelContext || document.modelContext || document.modelContext;
679
839
  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)");
840
+ throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (document.modelContext \u672A\u627E\u5230)");
681
841
  }
682
- let res = await mcp.executeTool(name, inputString);
842
+ const tools = await mcp.getTools();
843
+ const toolObj = tools.find((t) => t.name === name);
844
+ if (!toolObj) throw new Error(`Tool ${name} not found`);
845
+ let res = await mcp.executeTool(toolObj, inputString);
683
846
  if (typeof res === "string") {
684
847
  try {
685
848
  res = JSON.parse(res);
@@ -856,7 +1019,7 @@ async function tabsForwardCommand(tabid) {
856
1019
  // package.json
857
1020
  var package_default = {
858
1021
  name: "@opentiny/webmcp-cli",
859
- version: "0.0.4",
1022
+ version: "0.0.6",
860
1023
  type: "module",
861
1024
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
862
1025
  main: "dist/index.js",
@@ -896,6 +1059,7 @@ var package_default = {
896
1059
  clipboardy: "^5.3.1",
897
1060
  commander: "^11.1.0",
898
1061
  json5: "^2.2.3",
1062
+ marked: "^15.0.7",
899
1063
  picocolors: "^1.0.0",
900
1064
  "puppeteer-core": "^22.0.0"
901
1065
  },
@@ -956,9 +1120,66 @@ function cleanOldLogs(baseDir, logDir) {
956
1120
  } catch (e) {
957
1121
  }
958
1122
  }
1123
+ function formatLogResult(result) {
1124
+ if (result === void 0 || result === null) {
1125
+ return "null";
1126
+ }
1127
+ try {
1128
+ if (typeof result !== "object") {
1129
+ return String(result);
1130
+ }
1131
+ const clone = JSON.parse(JSON.stringify(result));
1132
+ let a11yTreeStr = "";
1133
+ let searchResultStr = "";
1134
+ if (clone && Array.isArray(clone.content) && clone.content.length > 0 && clone.content[0] && typeof clone.content[0].text === "string") {
1135
+ 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
+ }
1153
+ } 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
+ searchResultStr = text;
1155
+ clone.content[0].text = "[Search A11y Tree Result - See details below]";
1156
+ }
1157
+ }
1158
+ let out = JSON.stringify(clone, null, 2);
1159
+ if (a11yTreeStr) {
1160
+ out += `
1161
+
1162
+ [FORMATTED A11Y TREE]:
1163
+ ${a11yTreeStr}`;
1164
+ }
1165
+ if (searchResultStr) {
1166
+ out += `
1167
+
1168
+ [FORMATTED SEARCH RESULT]:
1169
+ ${searchResultStr}`;
1170
+ }
1171
+ return out;
1172
+ } catch (e) {
1173
+ try {
1174
+ return JSON.stringify(result, null, 2);
1175
+ } catch {
1176
+ return String(result);
1177
+ }
1178
+ }
1179
+ }
959
1180
  function writeLog(commandName, args, result, error) {
960
1181
  try {
961
- const baseDir = process.env.WEBMCP_WORKSPACE || path2.join(os2.homedir(), ".webmcp_chrome_profile");
1182
+ const baseDir = process.env.WEBMCP_WORKSPACE || path2.join(os3.homedir(), ".webmcp_chrome_profile");
962
1183
  const logDir = path2.join(baseDir, "logs");
963
1184
  if (!fs2.existsSync(logDir)) {
964
1185
  fs2.mkdirSync(logDir, { recursive: true });
@@ -981,15 +1202,8 @@ ${error instanceof Error ? error.stack : String(error)}
981
1202
  } else if (result) {
982
1203
  logText += `RESULT:
983
1204
  `;
984
- try {
985
- if (result && typeof result === "object") {
986
- logText += JSON.stringify(result, null, 2).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
987
- } else {
988
- logText += String(result).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
989
- }
990
- } catch {
991
- logText += JSON.stringify(result, null, 2).split("\n").map((line) => ` ${line}`).join("\n") + "\n";
992
- }
1205
+ const formattedResult = formatLogResult(result);
1206
+ logText += formattedResult.split("\n").map((line) => ` ${line}`).join("\n") + "\n";
993
1207
  } else {
994
1208
  logText += `RESULT: null
995
1209
  `;
@@ -1019,7 +1233,7 @@ program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D
1019
1233
  try {
1020
1234
  const result = await stateCommand(args);
1021
1235
  writeLog("state", args, result);
1022
- console.log(JSON.stringify(result, null, 2));
1236
+ console.log(formatLogResult(result));
1023
1237
  } catch (error) {
1024
1238
  handleCommandError(error, "state", args);
1025
1239
  }
@@ -1053,7 +1267,7 @@ program.command("run <toolName> [args...]").description("\u5411\u6307\u5B9A\u987
1053
1267
  tabid: parseTabId(options.tabid)
1054
1268
  });
1055
1269
  writeLog("run", { ...rawArgs, finalArgsJson }, result);
1056
- console.log(JSON.stringify(result, null, 2));
1270
+ console.log(formatLogResult(result));
1057
1271
  } catch (error) {
1058
1272
  handleCommandError(error, "run", { ...rawArgs, finalArgsJson });
1059
1273
  }