@opentiny/webmcp-cli 0.0.6 → 0.0.8

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/index.d.cts CHANGED
@@ -11,7 +11,10 @@ declare function stateCommand({ tabid }: {
11
11
  } | null)[];
12
12
  url: string;
13
13
  title: string;
14
- webmcpTools: any[];
14
+ webmcpTools: {
15
+ name: string;
16
+ description?: string;
17
+ }[];
15
18
  }>;
16
19
 
17
20
  declare function runCommand({ toolName, argsJson, tabid }: {
package/dist/index.d.ts CHANGED
@@ -11,7 +11,10 @@ declare function stateCommand({ tabid }: {
11
11
  } | null)[];
12
12
  url: string;
13
13
  title: string;
14
- webmcpTools: any[];
14
+ webmcpTools: {
15
+ name: string;
16
+ description?: string;
17
+ }[];
15
18
  }>;
16
19
 
17
20
  declare function runCommand({ toolName, argsJson, tabid }: {
package/dist/index.js CHANGED
@@ -9,11 +9,37 @@ import { spawn } from "child_process";
9
9
  import { fileURLToPath } from "url";
10
10
  var __filename = fileURLToPath(import.meta.url);
11
11
  var __dirname = path.dirname(__filename);
12
- var CDP_PORT = 9222;
13
- var CDP_URL = `http://localhost:${CDP_PORT}`;
12
+ function getCdpPort() {
13
+ const raw = process.env.WEBMCP_CDP_PORT;
14
+ if (raw) {
15
+ const port = Number.parseInt(raw, 10);
16
+ if (Number.isFinite(port) && port > 0) return port;
17
+ }
18
+ return 9222;
19
+ }
14
20
  function getWorkspaceDir() {
15
21
  return process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
16
22
  }
23
+ function shouldLaunchHeadless() {
24
+ if (process.env.WEBMCP_HEADLESS === "0") return false;
25
+ return process.env.WEBMCP_HEADLESS === "1" || process.env.CI === "true" || process.env.CI === "1";
26
+ }
27
+ function readActivePortFromProfile(userDataDir) {
28
+ try {
29
+ const content = fs.readFileSync(path.join(userDataDir, "DevToolsActivePort"), "utf-8");
30
+ const port = parseInt(content.split("\n")[0].trim(), 10);
31
+ return Number.isFinite(port) && port > 0 ? port : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
36
+ function candidateCdpBaseUrls(port) {
37
+ return [
38
+ `http://127.0.0.1:${port}`,
39
+ `http://[::1]:${port}`,
40
+ `http://localhost:${port}`
41
+ ];
42
+ }
17
43
  function getLastTabIdFilePath() {
18
44
  return path.join(getWorkspaceDir(), ".last-tab-id");
19
45
  }
@@ -97,6 +123,13 @@ async function connectPuppeteer(browserBaseUrl, attempt = 1, maxAttempts = 3) {
97
123
  }
98
124
  }
99
125
  function getDefaultBrowserPath() {
126
+ const envPath = process.env.CHROME_PATH || process.env.WEBMCP_CHROME_PATH || process.env.PUPPETEER_EXECUTABLE_PATH;
127
+ if (envPath && fs.existsSync(envPath)) {
128
+ return { path: envPath, name: "Chrome" };
129
+ }
130
+ if (envPath) {
131
+ console.log(pc.yellow(`\u6307\u5B9A\u7684\u6D4F\u89C8\u5668\u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5C06\u56DE\u9000\u5230\u81EA\u52A8\u68C0\u6D4B: ${envPath}`));
132
+ }
100
133
  const platform = os.platform();
101
134
  if (platform === "darwin") {
102
135
  const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
@@ -140,16 +173,30 @@ async function startBrowserInBackground() {
140
173
  if (!browserInfo || !fs.existsSync(browserInfo.path)) {
141
174
  throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6216 Edge \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
142
175
  }
143
- console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
144
- const userDataDir = process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
176
+ const cdpPort = getCdpPort();
177
+ console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${cdpPort})...`));
178
+ const userDataDir = getWorkspaceDir();
179
+ if (!fs.existsSync(userDataDir)) {
180
+ fs.mkdirSync(userDataDir, { recursive: true });
181
+ }
182
+ try {
183
+ fs.unlinkSync(path.join(userDataDir, "DevToolsActivePort"));
184
+ } catch {
185
+ }
145
186
  const launchArgs = [
146
- `--remote-debugging-port=${CDP_PORT}`,
187
+ `--remote-debugging-port=${cdpPort}`,
147
188
  `--user-data-dir=${userDataDir}`,
148
189
  "--no-first-run",
149
190
  "--no-default-browser-check"
150
191
  ];
151
- if (os.platform() === "win32") {
152
- launchArgs.push("--remote-debugging-address=127.0.0.1");
192
+ if (shouldLaunchHeadless()) {
193
+ launchArgs.push(
194
+ "--headless=new",
195
+ "--no-sandbox",
196
+ "--disable-setuid-sandbox",
197
+ "--disable-gpu",
198
+ "--disable-dev-shm-usage"
199
+ );
153
200
  }
154
201
  const child = spawn(
155
202
  browserInfo.path,
@@ -160,15 +207,22 @@ async function startBrowserInBackground() {
160
207
  }
161
208
  );
162
209
  child.unref();
163
- const pollUrls = [`http://127.0.0.1:${CDP_PORT}/json/version`, `http://localhost:${CDP_PORT}/json/version`];
164
- for (let i = 0; i < 30; i++) {
165
- for (const url of pollUrls) {
166
- try {
167
- await httpGet(url, 1e3);
168
- console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
169
- await new Promise((resolve) => setTimeout(resolve, 500));
170
- return;
171
- } catch {
210
+ for (let i = 0; i < 40; i++) {
211
+ const ports = /* @__PURE__ */ new Set([cdpPort]);
212
+ const activePort = readActivePortFromProfile(userDataDir);
213
+ if (activePort) ports.add(activePort);
214
+ for (const port of ports) {
215
+ for (const baseUrl of candidateCdpBaseUrls(port)) {
216
+ try {
217
+ await httpGet(`${baseUrl}/json/version`, 1e3);
218
+ if (port !== cdpPort) {
219
+ console.log(pc.yellow(`\u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u88AB\u5176\u4ED6\u7A0B\u5E8F\u5360\u7528\uFF0C${browserInfo.name} \u5DF2\u81EA\u52A8\u5207\u6362\u5230\u7AEF\u53E3 ${port}\u3002`));
220
+ }
221
+ console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\uFF08${baseUrl}\uFF09\u3002`));
222
+ await new Promise((resolve) => setTimeout(resolve, 500));
223
+ return baseUrl;
224
+ } catch {
225
+ }
172
226
  }
173
227
  }
174
228
  await new Promise((resolve) => setTimeout(resolve, 500));
@@ -176,13 +230,16 @@ async function startBrowserInBackground() {
176
230
  throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
177
231
  }
178
232
  async function connectBrowser() {
233
+ const userDataDir = getWorkspaceDir();
234
+ const cdpPort = getCdpPort();
235
+ const recordedPort = readActivePortFromProfile(userDataDir);
179
236
  const versionAddresses = [
180
- `http://127.0.0.1:${CDP_PORT}/json/version`,
181
- `http://localhost:${CDP_PORT}/json/version`
237
+ ...recordedPort && recordedPort !== cdpPort ? candidateCdpBaseUrls(recordedPort).map((u) => `${u}/json/version`) : [],
238
+ ...candidateCdpBaseUrls(cdpPort).map((u) => `${u}/json/version`)
182
239
  ];
183
240
  const existingUrl = await findAvailableCdpUrl(versionAddresses, 2);
184
241
  if (existingUrl) {
185
- console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${CDP_PORT} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
242
+ console.log(pc.yellow(`connectBrowser: \u68C0\u6D4B\u5230\u7AEF\u53E3 ${cdpPort} \u5DF2\u5C31\u7EEA\uFF0C\u6B63\u5728\u8FDE\u63A5 ${existingUrl}...`));
186
243
  try {
187
244
  const browser = await connectPuppeteer(existingUrl);
188
245
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${existingUrl}`));
@@ -194,25 +251,22 @@ async function connectBrowser() {
194
251
  );
195
252
  }
196
253
  }
197
- console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${CDP_PORT} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
254
+ console.log(pc.yellow(`connectBrowser: \u7AEF\u53E3 ${cdpPort} \u65E0\u54CD\u5E94\uFF0C\u6B63\u5728\u542F\u52A8\u65B0\u7684\u6D4F\u89C8\u5668\u5B9E\u4F8B...`));
255
+ let launchedUrl;
198
256
  try {
199
- await startBrowserInBackground();
257
+ launchedUrl = await startBrowserInBackground();
200
258
  } catch (launchError) {
201
259
  const msg = launchError instanceof Error ? launchError.message : String(launchError);
202
260
  console.error(pc.red(`\u65E0\u6CD5\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
203
261
  throw new Error("Browser launch failed.");
204
262
  }
205
- const launchedUrl = await findAvailableCdpUrl(versionAddresses, 5);
206
- if (!launchedUrl) {
207
- 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`);
208
- }
209
263
  try {
210
264
  console.log(pc.yellow(`connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u8FDE\u63A5 ${launchedUrl}...`));
211
265
  const browser = await connectPuppeteer(launchedUrl);
212
266
  console.log(pc.green(`connectBrowser: \u6210\u529F\u8FDE\u63A5 ${launchedUrl}`));
213
267
  return browser;
214
268
  } catch {
215
- 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`);
269
+ throw new Error(`\u65E0\u6CD5\u8FDE\u63A5\u5230\u6D4F\u89C8\u5668\uFF08${launchedUrl}\uFF09\uFF0C\u8BF7\u68C0\u67E5 Chrome/Edge \u662F\u5426\u5DF2\u5B89\u88C5\u3002`);
216
270
  }
217
271
  }
218
272
  async function getPageTargetId(page) {
@@ -270,10 +324,11 @@ async function getTargetPage(browser, tabid) {
270
324
  }
271
325
  if (!targetPage) {
272
326
  try {
273
- const urls = [
274
- `http://localhost:${CDP_PORT}/json/list`,
275
- `http://127.0.0.1:${CDP_PORT}/json/list`
276
- ];
327
+ const workspaceDir = getWorkspaceDir();
328
+ const cdpPort = getCdpPort();
329
+ const recordedPort = readActivePortFromProfile(workspaceDir);
330
+ const ports = recordedPort && recordedPort !== cdpPort ? [recordedPort, cdpPort] : [cdpPort];
331
+ const urls = ports.flatMap((port) => candidateCdpBaseUrls(port).map((u) => `${u}/json/list`));
277
332
  let activeTargetId = null;
278
333
  for (const url of urls) {
279
334
  try {
@@ -310,10 +365,10 @@ async function getTargetPage(browser, tabid) {
310
365
  return targetPage;
311
366
  }
312
367
  async function injectIntoPage(page) {
313
- await injectWebMCPPolyfillAndTools(page, true);
368
+ await injectWebMCPPolyfillAndTools(page);
314
369
  }
315
- async function injectWebMCPPolyfillAndTools(page, force = false) {
316
- const polyfillReady = !force && await page.evaluate(() => {
370
+ async function injectWebMCPPolyfillAndTools(page) {
371
+ const polyfillReady = await page.evaluate(() => {
317
372
  return !!window.__webmcpcli_init;
318
373
  }).catch(() => false);
319
374
  if (!polyfillReady) {
@@ -403,11 +458,19 @@ async function stateCommand({ tabid }) {
403
458
  const state = await page.evaluate(async () => {
404
459
  const url = document.URL;
405
460
  const title = document.title;
406
- const mcp = document.modelContext;
461
+ const mcp = document.modelContext || navigator.modelContext;
407
462
  let webmcpTools = [];
408
- if (mcp && typeof mcp.listTools === "function") {
409
- const toolsResult = await mcp.getTools();
410
- webmcpTools = toolsResult?.tools || toolsResult || [];
463
+ if (mcp && typeof mcp.getTools === "function") {
464
+ try {
465
+ const toolsResult = await mcp.getTools();
466
+ const list = toolsResult?.tools || toolsResult || [];
467
+ webmcpTools = list.filter((t) => t && typeof t.name === "string").map((t) => ({
468
+ name: t.name,
469
+ description: typeof t.description === "string" ? t.description.slice(0, 200) : void 0
470
+ }));
471
+ } catch {
472
+ webmcpTools = [];
473
+ }
411
474
  }
412
475
  return { url, title, webmcpTools };
413
476
  });
@@ -439,155 +502,6 @@ async function stateCommand({ tabid }) {
439
502
  }
440
503
  }
441
504
 
442
- // src/zhihu/create-article.ts
443
- import os2 from "os";
444
-
445
- // src/zhihu/markdown.ts
446
- import { Marked } from "marked";
447
- function stripLeadingH1(markdown) {
448
- return markdown.replace(/^#\s+[^\n]*\n+/, "");
449
- }
450
- function markdownToHtml(markdown) {
451
- const marked = new Marked({
452
- gfm: true,
453
- breaks: false,
454
- renderer: {
455
- code({ text, lang }) {
456
- const langClass = lang ? ` class="language-${lang}"` : "";
457
- const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
458
- return `<pre><code${langClass}>${escaped}</code></pre>`;
459
- },
460
- codespan({ text }) {
461
- const escaped = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
462
- return `<code>${escaped}</code>`;
463
- }
464
- }
465
- });
466
- return marked.parse(markdown);
467
- }
468
- function decodeBase64Content(content) {
469
- return Buffer.from(content, "base64").toString("utf-8");
470
- }
471
- var ZHIHU_WRITE_PAGE_RE = /^https:\/\/zhuanlan\.zhihu\.com\/(write|p\/\d+\/edit)/;
472
- function isZhihuWriteUrl(url) {
473
- return ZHIHU_WRITE_PAGE_RE.test(url);
474
- }
475
-
476
- // src/zhihu/create-article.ts
477
- function delay(ms) {
478
- return new Promise((resolve) => setTimeout(resolve, ms));
479
- }
480
- function getPasteModifier() {
481
- return os2.platform() === "darwin" ? "Meta" : "Control";
482
- }
483
- async function fillTitle(page, title) {
484
- await page.evaluate((titleText) => {
485
- const input = document.querySelector('textarea[placeholder*="\u6807\u9898"]') || document.querySelector(".WriteIndex-titleInput textarea") || document.querySelector(".WriteIndex-titleInput input");
486
- 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");
487
- const setter = Object.getOwnPropertyDescriptor(
488
- input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
489
- "value"
490
- )?.set;
491
- if (setter) {
492
- setter.call(input, titleText);
493
- } else {
494
- input.value = titleText;
495
- }
496
- input.dispatchEvent(new InputEvent("input", { bubbles: true, cancelable: true, data: titleText }));
497
- input.dispatchEvent(new Event("change", { bubbles: true }));
498
- if (input.value.trim() !== titleText.trim()) {
499
- throw new Error("\u6807\u9898\u586B\u5199\u5931\u8D25\uFF0C\u8BF7\u5237\u65B0\u9875\u9762\u540E\u91CD\u8BD5");
500
- }
501
- }, title);
502
- }
503
- async function pasteHtmlToEditor(page, html) {
504
- const browser = page.browser();
505
- const tempPage = await browser.newPage();
506
- const modifier = getPasteModifier();
507
- try {
508
- const fullHtml = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${html}</body></html>`;
509
- await tempPage.setContent(fullHtml, { waitUntil: "domcontentloaded" });
510
- await delay(300);
511
- await tempPage.click("body");
512
- await tempPage.keyboard.down(modifier);
513
- await tempPage.keyboard.press("KeyA");
514
- await tempPage.keyboard.up(modifier);
515
- await delay(100);
516
- await tempPage.keyboard.down(modifier);
517
- await tempPage.keyboard.press("KeyC");
518
- await tempPage.keyboard.up(modifier);
519
- await delay(300);
520
- } finally {
521
- await tempPage.close();
522
- }
523
- await page.bringToFront();
524
- await page.waitForSelector(".public-DraftEditor-content", { timeout: 1e4 });
525
- await page.click(".public-DraftEditor-content");
526
- await delay(200);
527
- await page.keyboard.down(modifier);
528
- await page.keyboard.press("KeyA");
529
- await page.keyboard.up(modifier);
530
- await delay(100);
531
- await page.keyboard.press("Backspace");
532
- await delay(200);
533
- await page.keyboard.down(modifier);
534
- await page.keyboard.press("KeyV");
535
- await page.keyboard.up(modifier);
536
- await delay(1500);
537
- const probe = html.replace(/<[^>]+>/g, "").trim().slice(0, 15);
538
- const verifyResult = await page.evaluate((probeText) => {
539
- const editor = document.querySelector(".public-DraftEditor-content");
540
- if (!editor) return { ok: false, reason: "no editor" };
541
- const text = (editor.innerText || "").trim();
542
- if (!text) return { ok: false, reason: "empty" };
543
- const inner = editor.innerHTML;
544
- const hasRichFormat = inner.includes("data-text") || /font-weight:\s*bold|font-style:\s*italic|code-block|blockquote/i.test(inner);
545
- const normalizedText = text.replace(/\s+/g, "");
546
- const normalizedProbe = probeText.replace(/\s+/g, "");
547
- const hasProbe = normalizedText.includes(normalizedProbe);
548
- return {
549
- ok: text.length > 20 && hasProbe && (hasRichFormat || text.length > 50),
550
- reason: !hasProbe ? "probe mismatch" : !hasRichFormat && text.length <= 50 ? "no rich format" : void 0
551
- };
552
- }, probe);
553
- if (!verifyResult.ok) {
554
- throw new Error(
555
- `\u6B63\u6587\u586B\u5199\u5931\u8D25\uFF08Markdown \u8F6C HTML \u7C98\u8D34\u672A\u751F\u6548\uFF09${verifyResult.reason ? `: ${verifyResult.reason}` : ""}`
556
- );
557
- }
558
- }
559
- async function zhihuCreateArticle(page, args) {
560
- const { title, content } = args;
561
- if (!title?.trim()) throw new Error("\u53C2\u6570 title \u4E0D\u80FD\u4E3A\u7A7A");
562
- if (!content?.trim()) throw new Error("\u53C2\u6570 content \u4E0D\u80FD\u4E3A\u7A7A");
563
- const url = page.url();
564
- if (!isZhihuWriteUrl(url)) {
565
- 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");
566
- }
567
- let markdown;
568
- try {
569
- markdown = stripLeadingH1(decodeBase64Content(content));
570
- } catch {
571
- 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");
572
- }
573
- if (!markdown.trim()) {
574
- throw new Error("\u6B63\u6587\u5185\u5BB9\u4E3A\u7A7A\uFF0C\u8BF7\u68C0\u67E5 Markdown \u6587\u4EF6");
575
- }
576
- const html = markdownToHtml(markdown);
577
- await fillTitle(page, title.trim());
578
- await delay(500);
579
- await pasteHtmlToEditor(page, html);
580
- const result = {
581
- success: true,
582
- 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",
583
- title: title.trim(),
584
- contentLength: markdown.length
585
- };
586
- return {
587
- content: [{ type: "text", text: JSON.stringify(result) }]
588
- };
589
- }
590
-
591
505
  // src/commands/run.ts
592
506
  async function runCommand({
593
507
  toolName,
@@ -598,18 +512,8 @@ async function runCommand({
598
512
  try {
599
513
  const page = await getTargetPage(browser, tabid);
600
514
  const urlBefore = page.url();
515
+ await injectIntoPage(page);
601
516
  const cleanedArgs = argsJson.trim();
602
- if (toolName === "create_article") {
603
- let parsedArgs;
604
- try {
605
- parsedArgs = JSON.parse(cleanedArgs);
606
- } catch {
607
- throw new Error("\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON");
608
- }
609
- if (isZhihuWriteUrl(page.url())) {
610
- return await zhihuCreateArticle(page, parsedArgs);
611
- }
612
- }
613
517
  let result;
614
518
  try {
615
519
  result = await page.evaluate(async (name, inputString) => {