@opentiny/webmcp-cli 0.0.1-alpha.5 → 0.0.1-alpha.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,8 +3,180 @@
3
3
  // src/bin.ts
4
4
  import { Command } from "commander";
5
5
  import pc2 from "picocolors";
6
+ import { readFileSync as readFileSync2 } from "fs";
7
+ import { resolve as resolve2 } from "path";
8
+
9
+ // src/expand-file-refs.ts
6
10
  import { readFileSync } from "fs";
7
- import { resolve } from "path";
11
+ import { dirname, isAbsolute, normalize, resolve } from "path";
12
+ import { fileURLToPath } from "url";
13
+ var JSON_STRING_LITERAL = /"(?:[^"\\]|\\.)*"/g;
14
+ var UNQUOTED_FILE_REF = /(?<![\\"])@(base64)?file:([^\s"',}\]]+)/g;
15
+ var FILE_REF_PREFIX = /^@(base64)?file:(.+)$/s;
16
+ function resolveFilePath(filePath, baseDir = process.cwd()) {
17
+ let normalizedPath = filePath.trim();
18
+ if (normalizedPath.startsWith('"') && normalizedPath.endsWith('"') || normalizedPath.startsWith("'") && normalizedPath.endsWith("'")) {
19
+ normalizedPath = normalizedPath.slice(1, -1).trim();
20
+ }
21
+ if (normalizedPath.startsWith("file://")) {
22
+ normalizedPath = fileURLToPath(normalizedPath);
23
+ }
24
+ if (isAbsolute(normalizedPath)) {
25
+ return normalize(normalizedPath);
26
+ }
27
+ return resolve(baseDir, normalizedPath);
28
+ }
29
+ function decodeFilePath(rawPath) {
30
+ return rawPath.replace(/\\\\/g, "\\");
31
+ }
32
+ function readFileRefContent(filePath, baseDir, base64) {
33
+ const absPath = resolveFilePath(filePath, baseDir);
34
+ let content;
35
+ try {
36
+ content = readFileSync(absPath, "utf-8");
37
+ } catch (e) {
38
+ throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}" (${absPath}): ${e.message}`);
39
+ }
40
+ return base64 ? Buffer.from(content, "utf-8").toString("base64") : content;
41
+ }
42
+ function expandFileRefValue(rawInner, baseDir) {
43
+ const match = rawInner.match(FILE_REF_PREFIX);
44
+ if (!match) {
45
+ return null;
46
+ }
47
+ const [, base64Flag, rawPath] = match;
48
+ const filePath = decodeFilePath(rawPath);
49
+ const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
50
+ return JSON.stringify(value);
51
+ }
52
+ function expandQuotedFileRefs(argsJson, baseDir) {
53
+ return argsJson.replace(JSON_STRING_LITERAL, (jsonStringLiteral) => {
54
+ const rawInner = jsonStringLiteral.slice(1, -1);
55
+ const replacement = expandFileRefValue(rawInner, baseDir);
56
+ return replacement ?? jsonStringLiteral;
57
+ });
58
+ }
59
+ function expandUnquotedFileRefs(argsJson, baseDir) {
60
+ return argsJson.replace(UNQUOTED_FILE_REF, (match, base64Flag, filePath) => {
61
+ const value = readFileRefContent(filePath, baseDir, Boolean(base64Flag));
62
+ return JSON.stringify(value);
63
+ });
64
+ }
65
+ function expandFileRefs(argsJson, baseDir = process.cwd()) {
66
+ const withQuotedExpanded = expandQuotedFileRefs(argsJson, baseDir);
67
+ return expandUnquotedFileRefs(withQuotedExpanded, baseDir);
68
+ }
69
+ function getFileBaseDir(fileOption) {
70
+ if (!fileOption) {
71
+ return process.cwd();
72
+ }
73
+ return dirname(resolve(process.cwd(), fileOption));
74
+ }
75
+
76
+ // src/parse-run-args.ts
77
+ import JSON5 from "json5";
78
+ function cleanJsonString(str) {
79
+ let cleaned = str.trim();
80
+ if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
81
+ cleaned = cleaned.slice(1, -1).trim();
82
+ }
83
+ return cleaned;
84
+ }
85
+ function looksLikeObjectBody(raw) {
86
+ return /^("?[\w-]+"?\s*:)/.test(raw);
87
+ }
88
+ function joinShellArgs(args) {
89
+ const cleaned = args.map(cleanJsonString);
90
+ const first = cleaned[0] ?? "";
91
+ if (first.startsWith("{")) {
92
+ return cleaned.join(" ");
93
+ }
94
+ if (looksLikeObjectBody(first)) {
95
+ let joined = cleaned.join(" ");
96
+ if (!joined.startsWith("{")) {
97
+ joined = `{${joined}}`;
98
+ }
99
+ if (!joined.endsWith("}")) {
100
+ joined = `${joined}}`;
101
+ }
102
+ return joined;
103
+ }
104
+ return cleaned.join(" ");
105
+ }
106
+ function insertMissingCommas(raw) {
107
+ return raw.replace(/"(\s+)("?[\w-]+"?\s*:)/g, '",$2');
108
+ }
109
+ function quoteUnquotedStringValues(raw) {
110
+ return raw.replace(
111
+ /([{,]\s*(?:"([^"]+)"|([\w-]+))\s*:\s*)(?!"@(?:base64)?file:|"|true|false|null|-?\d|\{|\[)([^",}\]]+?)(?=\s*[,}])/g,
112
+ (_match, prefix, _quotedKey, _unquotedKey, value) => {
113
+ const trimmed = value.trim();
114
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(trimmed)) {
115
+ return prefix + trimmed;
116
+ }
117
+ return prefix + JSON.stringify(trimmed);
118
+ }
119
+ );
120
+ }
121
+ function normalizeShellDamagedJson(raw) {
122
+ let normalized = raw.trim();
123
+ normalized = insertMissingCommas(normalized);
124
+ normalized = quoteUnquotedStringValues(normalized);
125
+ return normalized;
126
+ }
127
+ function buildArgsFromKeyValuePairs(args) {
128
+ const obj = {};
129
+ for (const pair of args) {
130
+ const index = pair.indexOf("=");
131
+ if (index === -1) {
132
+ throw new Error(`\u65E0\u6548\u7684\u53C2\u6570\u683C\u5F0F "${pair}"\u3002\u8BF7\u4F7F\u7528 key=value\uFF0C\u6216 JSON / -f \u6587\u4EF6\u3002`);
133
+ }
134
+ const key = pair.substring(0, index).trim();
135
+ let val = cleanJsonString(pair.substring(index + 1).trim());
136
+ try {
137
+ obj[key] = JSON.parse(val);
138
+ } catch {
139
+ obj[key] = val;
140
+ }
141
+ }
142
+ return JSON.stringify(obj);
143
+ }
144
+ function parseArgsJson(raw) {
145
+ const trimmed = cleanJsonString(raw);
146
+ try {
147
+ return JSON.stringify(JSON5.parse(trimmed));
148
+ } catch (firstError) {
149
+ const repaired = normalizeShellDamagedJson(trimmed);
150
+ try {
151
+ return JSON.stringify(JSON5.parse(repaired));
152
+ } catch {
153
+ const hint = `\u63D0\u793A\uFF1Abash \u4E2D JSON \u5FC5\u987B\u7528\u5355\u5F15\u53F7\u5305\u88F9\uFF0C\u4F8B\u5982 '{"title":"\u6807\u9898","content":"@base64file:./article.md"}'\uFF1B\u6216\u4F7F\u7528 key=value\uFF1Atitle=\u6807\u9898 content=@base64file:./article.md\uFF1B\u6216\u4F7F\u7528 -f args.json\u3002`;
154
+ const message = firstError instanceof Error ? firstError.message : String(firstError);
155
+ throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON \u6216 JS \u5BF9\u8C61: ${message}
156
+ ${hint}`);
157
+ }
158
+ }
159
+ }
160
+ function prepareRunArgsJson(args, fileContent, baseDir) {
161
+ let raw = "";
162
+ if (fileContent !== void 0) {
163
+ raw = fileContent.trim();
164
+ } else if (args.length > 0) {
165
+ const joined = joinShellArgs(args);
166
+ if (joined.startsWith("{") || looksLikeObjectBody(joined)) {
167
+ raw = joined;
168
+ } else if (args.every((arg) => arg.includes("="))) {
169
+ raw = buildArgsFromKeyValuePairs(args);
170
+ } else {
171
+ raw = joined;
172
+ }
173
+ }
174
+ if (!raw) {
175
+ throw new Error("\u5FC5\u987B\u63D0\u4F9B\u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
176
+ }
177
+ const expanded = expandFileRefs(raw, baseDir);
178
+ return parseArgsJson(expanded);
179
+ }
8
180
 
9
181
  // src/browser.ts
10
182
  import puppeteer from "puppeteer-core";
@@ -14,13 +186,13 @@ import os from "os";
14
186
  import path from "path";
15
187
  import http from "http";
16
188
  import { spawn } from "child_process";
17
- import { fileURLToPath } from "url";
18
- var __filename = fileURLToPath(import.meta.url);
189
+ import { fileURLToPath as fileURLToPath2 } from "url";
190
+ var __filename = fileURLToPath2(import.meta.url);
19
191
  var __dirname = path.dirname(__filename);
20
192
  var CDP_PORT = 9222;
21
193
  var CDP_URL = `http://localhost:${CDP_PORT}`;
22
194
  function httpGet(url, timeoutMs = 1500) {
23
- return new Promise((resolve2, reject) => {
195
+ return new Promise((resolve3, reject) => {
24
196
  const req = http.get(url, { timeout: timeoutMs }, (res) => {
25
197
  let data = "";
26
198
  res.on("data", (chunk) => {
@@ -28,7 +200,7 @@ function httpGet(url, timeoutMs = 1500) {
28
200
  });
29
201
  res.on("end", () => {
30
202
  if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
31
- resolve2(data);
203
+ resolve3(data);
32
204
  } else {
33
205
  reject(new Error(`HTTP ${res.statusCode}`));
34
206
  }
@@ -138,7 +310,7 @@ async function startBrowserInBackground() {
138
310
  }
139
311
  } catch (e) {
140
312
  }
141
- await new Promise((resolve2) => setTimeout(resolve2, 500));
313
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
142
314
  }
143
315
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
144
316
  }
@@ -332,7 +504,7 @@ async function injectWebMCPPolyfillAndTools(page2, force = false) {
332
504
  const msg = err instanceof Error ? err.message : String(err);
333
505
  throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
334
506
  }
335
- await new Promise((resolve2) => setTimeout(resolve2, 300));
507
+ await new Promise((resolve3) => setTimeout(resolve3, 300));
336
508
  }
337
509
  await injectDomainTools(page2);
338
510
  }
@@ -420,7 +592,7 @@ async function stateCommand({ tabid: tabid2 }) {
420
592
  if (pUrl.startsWith("devtools://")) return null;
421
593
  const pTitle = await Promise.race([
422
594
  p.title().catch(() => "Unknown"),
423
- new Promise((resolve2) => setTimeout(() => resolve2("Unknown"), 500))
595
+ new Promise((resolve3) => setTimeout(() => resolve3("Unknown"), 500))
424
596
  ]);
425
597
  return {
426
598
  // 使用真实的 Chrome target ID,而非数组下标
@@ -440,7 +612,6 @@ async function stateCommand({ tabid: tabid2 }) {
440
612
  }
441
613
 
442
614
  // src/commands/run.ts
443
- import JSON5 from "json5";
444
615
  async function runCommand({
445
616
  toolName,
446
617
  argsJson,
@@ -449,16 +620,7 @@ async function runCommand({
449
620
  const browser2 = await connectBrowser();
450
621
  try {
451
622
  const page2 = await getTargetPage(browser2, tabid2);
452
- let cleanedArgs = argsJson.trim();
453
- if (cleanedArgs.startsWith("'") && cleanedArgs.endsWith("'")) {
454
- cleanedArgs = cleanedArgs.slice(1, -1).trim();
455
- }
456
- try {
457
- const obj = JSON5.parse(cleanedArgs);
458
- cleanedArgs = JSON.stringify(obj);
459
- } catch (e) {
460
- throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON \u6216 JS \u5BF9\u8C61: ${e.message}`);
461
- }
623
+ const cleanedArgs = argsJson.trim();
462
624
  const result2 = await page2.evaluate(async (name, inputString) => {
463
625
  const mcp = navigator.modelContextTesting || navigator.modelContext;
464
626
  if (!mcp || typeof mcp.executeTool !== "function") {
@@ -468,9 +630,17 @@ async function runCommand({
468
630
  if (typeof res === "string") {
469
631
  try {
470
632
  res = JSON.parse(res);
471
- } catch (e) {
633
+ } catch {
472
634
  }
473
635
  }
636
+ if (res === void 0 || res === null) {
637
+ throw new Error("\u5DE5\u5177 execute \u672A\u8FD4\u56DE\u7ED3\u679C\uFF0C\u8BF7\u68C0\u67E5\u5DE5\u5177\u5B9E\u73B0\u662F\u5426\u7F3A\u5C11 return");
638
+ }
639
+ if (typeof res === "object" && res.success === false) {
640
+ const failed = res;
641
+ throw new Error(failed.error || failed.message || "\u5DE5\u5177\u6267\u884C\u5931\u8D25");
642
+ }
643
+ return res;
474
644
  }, toolName, cleanedArgs);
475
645
  return result2;
476
646
  } finally {
@@ -531,7 +701,7 @@ async function tabsOpenCommand(url) {
531
701
  url = normalizeUrl(url);
532
702
  await page2.goto(url, { waitUntil: "domcontentloaded" });
533
703
  await injectIntoPage(page2);
534
- await new Promise((resolve2) => setTimeout(resolve2, 500));
704
+ await new Promise((resolve3) => setTimeout(resolve3, 500));
535
705
  return {
536
706
  success: true,
537
707
  tabid: await getPageTargetId(page2),
@@ -632,7 +802,7 @@ async function tabsForwardCommand(tabid2) {
632
802
  // package.json
633
803
  var package_default = {
634
804
  name: "@opentiny/webmcp-cli",
635
- version: "0.0.1-alpha.5",
805
+ version: "0.0.1-alpha.6",
636
806
  type: "module",
637
807
  description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
638
808
  main: "dist/index.js",
@@ -686,13 +856,6 @@ var package_default = {
686
856
 
687
857
  // src/bin.ts
688
858
  var program = new Command();
689
- function cleanJsonString(str) {
690
- let cleaned = str.trim();
691
- if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
692
- cleaned = cleaned.slice(1, -1).trim();
693
- }
694
- return cleaned;
695
- }
696
859
  function parseTabId(id) {
697
860
  if (!id) return void 0;
698
861
  return id;
@@ -702,19 +865,6 @@ function handleCommandError(error, commandName) {
702
865
  console.error(pc2.red(`Error executing ${commandName} command: ${msg}`));
703
866
  process.exit(1);
704
867
  }
705
- function expandFileRefs(argsJson) {
706
- return argsJson.replace(/"@(base64)?file:([^"]+)"/g, (_match, base64Flag, filePath) => {
707
- const absPath = resolve(process.cwd(), filePath);
708
- let content;
709
- try {
710
- content = readFileSync(absPath, "utf-8");
711
- } catch (e) {
712
- throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6\u5F15\u7528 "${filePath}": ${e.message}`);
713
- }
714
- const value = base64Flag ? Buffer.from(content, "utf-8").toString("base64") : content;
715
- return JSON.stringify(value);
716
- });
717
- }
718
868
  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) => {
719
869
  const opts = thisCommand.opts();
720
870
  if (opts.workspace) {
@@ -738,42 +888,18 @@ program.command("run <toolName> [args...]").description("\u5411\u6307\u5B9A\u987
738
888
  \u793A\u4F8B\uFF1Awebmcp-cli run mytool '{"content":"@base64file:./doc.md"}'`
739
889
  ).action(async (toolName, args, options) => {
740
890
  try {
741
- let finalArgsJson = "";
891
+ let fileContent;
892
+ let fileBaseDir = process.cwd();
742
893
  if (options.file) {
743
- const filePath = resolve(process.cwd(), options.file);
894
+ const filePath = resolve2(process.cwd(), options.file);
895
+ fileBaseDir = getFileBaseDir(options.file);
744
896
  try {
745
- finalArgsJson = readFileSync(filePath, "utf-8").trim();
897
+ fileContent = readFileSync2(filePath, "utf-8");
746
898
  } catch (e) {
747
899
  throw new Error(`\u65E0\u6CD5\u8BFB\u53D6\u6587\u4EF6 "${filePath}": ${e.message}`);
748
900
  }
749
- } else if (args && args.length > 0) {
750
- const firstArg = args[0].trim();
751
- const cleanedFirstArg = cleanJsonString(firstArg);
752
- if (cleanedFirstArg.startsWith("{") && cleanedFirstArg.endsWith("}")) {
753
- finalArgsJson = args.map(cleanJsonString).join(" ");
754
- } else {
755
- const obj = {};
756
- for (const pair of args) {
757
- const index = pair.indexOf("=");
758
- if (index === -1) {
759
- throw new Error(`\u65E0\u6548\u7684\u53C2\u6570\u683C\u5F0F "${pair}"\u3002\u5FC5\u987B\u662F JSON \u5B57\u7B26\u4E32\u6216 key=value \u683C\u5F0F\u3002`);
760
- }
761
- const key = pair.substring(0, index).trim();
762
- let val = pair.substring(index + 1).trim();
763
- val = cleanJsonString(val);
764
- try {
765
- obj[key] = JSON.parse(val);
766
- } catch {
767
- obj[key] = val;
768
- }
769
- }
770
- finalArgsJson = JSON.stringify(obj);
771
- }
772
- }
773
- if (!finalArgsJson) {
774
- throw new Error("\u5FC5\u987B\u63D0\u4F9B\u53C2\u6570\u6216\u901A\u8FC7 -f/--file \u6307\u5B9A\u53C2\u6570\u6587\u4EF6");
775
901
  }
776
- finalArgsJson = expandFileRefs(finalArgsJson);
902
+ const finalArgsJson = prepareRunArgsJson(args ?? [], fileContent, fileBaseDir);
777
903
  const result2 = await runCommand({
778
904
  toolName,
779
905
  argsJson: finalArgsJson,