@opentiny/webmcp-cli 0.0.1-alpha.1 → 0.0.1-alpha.3
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/README.md +10 -6
- package/dist/bin.cjs +393 -144
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +393 -144
- package/dist/bin.js.map +1 -1
- package/dist/index.cjs +79 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +79 -44
- package/dist/index.js.map +1 -1
- package/dist/inject-bundle.js +3 -3
- package/dist/webmcp-tools/juejin.cn.js +72 -0
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
8
8
|
|
|
9
9
|
// src/bin.ts
|
|
10
10
|
import { Command } from "commander";
|
|
11
|
-
import
|
|
11
|
+
import pc2 from "picocolors";
|
|
12
12
|
|
|
13
13
|
// src/browser.ts
|
|
14
14
|
import puppeteer from "puppeteer-core";
|
|
@@ -94,31 +94,54 @@ async function killProcessOnPortIfZombie(port) {
|
|
|
94
94
|
} catch (e) {
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function getDefaultBrowserPath() {
|
|
98
98
|
const platform = os.platform();
|
|
99
99
|
if (platform === "darwin") {
|
|
100
|
-
|
|
100
|
+
const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
|
|
101
|
+
if (fs.existsSync(chromePath)) return { path: chromePath, name: "Chrome" };
|
|
102
|
+
const edgePath = "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge";
|
|
103
|
+
if (fs.existsSync(edgePath)) return { path: edgePath, name: "Edge" };
|
|
104
|
+
return null;
|
|
101
105
|
} else if (platform === "win32") {
|
|
102
|
-
const
|
|
106
|
+
const chromePaths = [
|
|
103
107
|
process.env.LOCALAPPDATA + "\\Google\\Chrome\\Application\\chrome.exe",
|
|
104
108
|
process.env.PROGRAMFILES + "\\Google\\Chrome\\Application\\chrome.exe",
|
|
105
109
|
process.env["PROGRAMFILES(X86)"] + "\\Google\\Chrome\\Application\\chrome.exe"
|
|
106
110
|
];
|
|
107
|
-
|
|
111
|
+
const foundChrome = chromePaths.find((p) => fs.existsSync(p));
|
|
112
|
+
if (foundChrome) return { path: foundChrome, name: "Chrome" };
|
|
113
|
+
const edgePaths = [
|
|
114
|
+
process.env.LOCALAPPDATA + "\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
115
|
+
process.env.PROGRAMFILES + "\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
116
|
+
process.env["PROGRAMFILES(X86)"] + "\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
117
|
+
"C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
|
|
118
|
+
"C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"
|
|
119
|
+
];
|
|
120
|
+
const foundEdge = edgePaths.find((p) => fs.existsSync(p));
|
|
121
|
+
if (foundEdge) return { path: foundEdge, name: "Edge" };
|
|
122
|
+
return null;
|
|
108
123
|
} else {
|
|
109
|
-
const paths = [
|
|
110
|
-
|
|
124
|
+
const paths = [
|
|
125
|
+
{ p: "/usr/bin/google-chrome", name: "Chrome" },
|
|
126
|
+
{ p: "/usr/bin/google-chrome-stable", name: "Chrome" },
|
|
127
|
+
{ p: "/usr/bin/chromium", name: "Chromium" },
|
|
128
|
+
{ p: "/usr/bin/chromium-browser", name: "Chromium" },
|
|
129
|
+
{ p: "/usr/bin/microsoft-edge", name: "Edge" },
|
|
130
|
+
{ p: "/usr/bin/microsoft-edge-stable", name: "Edge" }
|
|
131
|
+
];
|
|
132
|
+
const found = paths.find(({ p }) => fs.existsSync(p));
|
|
133
|
+
return found ? { path: found.p, name: found.name } : null;
|
|
111
134
|
}
|
|
112
135
|
}
|
|
113
|
-
async function
|
|
114
|
-
const
|
|
115
|
-
if (!
|
|
116
|
-
throw new Error("\u65E0\u6CD5\u5728\u7CFB\u7EDF\u4E2D\u627E\u5230 Chrome \u6D4F\u89C8\u5668\u7684\u9ED8\u8BA4\u5B89\u88C5\u8DEF\u5F84\u3002");
|
|
136
|
+
async function startBrowserInBackground() {
|
|
137
|
+
const browserInfo = getDefaultBrowserPath();
|
|
138
|
+
if (!browserInfo || !fs.existsSync(browserInfo.path)) {
|
|
139
|
+
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");
|
|
117
140
|
}
|
|
118
|
-
console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0
|
|
141
|
+
console.log(pc.yellow(`\u6B63\u5728\u542F\u52A8\u540E\u53F0 ${browserInfo.name} \u5B9E\u4F8B (\u7AEF\u53E3: ${CDP_PORT})...`));
|
|
119
142
|
const userDataDir = process.env.WEBMCP_WORKSPACE || path.join(os.homedir(), ".webmcp_chrome_profile");
|
|
120
143
|
const child = spawn(
|
|
121
|
-
|
|
144
|
+
browserInfo.path,
|
|
122
145
|
[
|
|
123
146
|
`--remote-debugging-port=${CDP_PORT}`,
|
|
124
147
|
`--user-data-dir=${userDataDir}`,
|
|
@@ -138,7 +161,7 @@ async function startChromeInBackground() {
|
|
|
138
161
|
try {
|
|
139
162
|
const response = await fetchWithTimeout(url, 1e3);
|
|
140
163
|
if (response.ok) {
|
|
141
|
-
console.log(pc.green(
|
|
164
|
+
console.log(pc.green(`${browserInfo.name} \u542F\u52A8\u5E76\u5C31\u7EEA\u3002`));
|
|
142
165
|
return;
|
|
143
166
|
}
|
|
144
167
|
} catch (err) {
|
|
@@ -148,7 +171,7 @@ async function startChromeInBackground() {
|
|
|
148
171
|
}
|
|
149
172
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
150
173
|
}
|
|
151
|
-
throw new Error(
|
|
174
|
+
throw new Error(`${browserInfo.name} \u542F\u52A8\u8D85\u65F6\uFF0C\u65E0\u6CD5\u8FDE\u63A5\u5230 CDP \u7AEF\u53E3\u3002`);
|
|
152
175
|
}
|
|
153
176
|
async function connectBrowser() {
|
|
154
177
|
const targetFilter = (target) => {
|
|
@@ -173,7 +196,7 @@ async function connectBrowser() {
|
|
|
173
196
|
throw new Error("127.0.0.1 CDP port not responding");
|
|
174
197
|
}
|
|
175
198
|
console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
|
|
176
|
-
const
|
|
199
|
+
const browser2 = await promiseWithTimeout(
|
|
177
200
|
puppeteer.connect({
|
|
178
201
|
browserURL: `http://127.0.0.1:${CDP_PORT}`,
|
|
179
202
|
defaultViewport: null,
|
|
@@ -183,7 +206,7 @@ async function connectBrowser() {
|
|
|
183
206
|
"puppeteer.connect to 127.0.0.1 timed out"
|
|
184
207
|
);
|
|
185
208
|
console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
|
|
186
|
-
return
|
|
209
|
+
return browser2;
|
|
187
210
|
} catch (error) {
|
|
188
211
|
try {
|
|
189
212
|
const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
|
|
@@ -191,7 +214,7 @@ async function connectBrowser() {
|
|
|
191
214
|
throw new Error("localhost CDP port not responding");
|
|
192
215
|
}
|
|
193
216
|
console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
|
|
194
|
-
const
|
|
217
|
+
const browser2 = await promiseWithTimeout(
|
|
195
218
|
puppeteer.connect({
|
|
196
219
|
browserURL: `http://localhost:${CDP_PORT}`,
|
|
197
220
|
defaultViewport: null,
|
|
@@ -201,19 +224,19 @@ async function connectBrowser() {
|
|
|
201
224
|
"puppeteer.connect to localhost timed out"
|
|
202
225
|
);
|
|
203
226
|
console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
|
|
204
|
-
return
|
|
227
|
+
return browser2;
|
|
205
228
|
} catch (error2) {
|
|
206
229
|
console.log(pc.yellow(`connectBrowser: \u8FDE\u63A5\u5931\u8D25\uFF0C\u5C06\u5C1D\u8BD5\u5524\u8D77\u6D4F\u89C8\u5668\u3002\u9519\u8BEF\u539F\u56E0: ${error2 instanceof Error ? error2.message : String(error2)}`));
|
|
207
230
|
try {
|
|
208
231
|
await killProcessOnPortIfZombie(CDP_PORT);
|
|
209
|
-
await
|
|
232
|
+
await startBrowserInBackground();
|
|
210
233
|
try {
|
|
211
234
|
const is127Ready = await checkCdpReady(`http://127.0.0.1:${CDP_PORT}/json/version`, 3);
|
|
212
235
|
if (!is127Ready) {
|
|
213
236
|
throw new Error("127.0.0.1 CDP port not responding after launch");
|
|
214
237
|
}
|
|
215
238
|
console.log(pc.yellow("connectBrowser: \u6D4F\u89C8\u5668\u5DF2\u542F\u52A8\uFF0C\u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 127.0.0.1:9222..."));
|
|
216
|
-
const
|
|
239
|
+
const browser2 = await promiseWithTimeout(
|
|
217
240
|
puppeteer.connect({
|
|
218
241
|
browserURL: `http://127.0.0.1:${CDP_PORT}`,
|
|
219
242
|
defaultViewport: null,
|
|
@@ -223,14 +246,14 @@ async function connectBrowser() {
|
|
|
223
246
|
"puppeteer.connect to 127.0.0.1 after launch timed out"
|
|
224
247
|
);
|
|
225
248
|
console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 127.0.0.1:9222"));
|
|
226
|
-
return
|
|
249
|
+
return browser2;
|
|
227
250
|
} catch (e) {
|
|
228
251
|
const isLocalhostReady = await checkCdpReady(`http://localhost:${CDP_PORT}/json/version`, 3);
|
|
229
252
|
if (!isLocalhostReady) {
|
|
230
253
|
throw new Error("localhost CDP port not responding after launch");
|
|
231
254
|
}
|
|
232
255
|
console.log(pc.yellow("connectBrowser: \u6B63\u5728\u5C1D\u8BD5\u8FDE\u63A5 localhost:9222..."));
|
|
233
|
-
const
|
|
256
|
+
const browser2 = await promiseWithTimeout(
|
|
234
257
|
puppeteer.connect({
|
|
235
258
|
browserURL: `http://localhost:${CDP_PORT}`,
|
|
236
259
|
defaultViewport: null,
|
|
@@ -240,20 +263,20 @@ async function connectBrowser() {
|
|
|
240
263
|
"puppeteer.connect to localhost after launch timed out"
|
|
241
264
|
);
|
|
242
265
|
console.log(pc.green("connectBrowser: \u6210\u529F\u8FDE\u63A5 localhost:9222"));
|
|
243
|
-
return
|
|
266
|
+
return browser2;
|
|
244
267
|
}
|
|
245
268
|
} catch (launchError) {
|
|
246
269
|
const msg = launchError instanceof Error ? launchError.message : String(launchError);
|
|
247
270
|
console.error(pc.red(`\u65E0\u6CD5\u8FDE\u63A5\u6216\u542F\u52A8\u6D4F\u89C8\u5668: ${msg}`));
|
|
248
|
-
console.error(pc.yellow(`\u{1F4A1} \u63D0\u793A\uFF1A\u7531\u4E8E\u6211\u4EEC\u8981\u4F7F\u7528\u4F60\u65E5\u5E38\u7684\u9ED8\u8BA4\u6D4F\u89C8\u5668\uFF08\u5305\u542B\u4F60\u7684\u4E66\u7B7E
|
|
249
|
-
console.error(pc.yellow(`\u{1F449} \u89E3\u51B3\u529E\u6CD5\uFF1A\u8BF7\u5148\u5B8C\u5168\u9000\u51FA\u5F53\u524D\u7684 Chrome \u6D4F\u89C8\u5668\uFF08\u5728 Mac \u4E0A\u6309 Cmd+Q\uFF09\uFF0C\u7136\u540E\u518D\u91CD\u65B0\u8FD0\u884C\u547D\u4EE4\u3002`));
|
|
271
|
+
console.error(pc.yellow(`\u{1F4A1} \u63D0\u793A\uFF1A\u7531\u4E8E\u6211\u4EEC\u8981\u4F7F\u7528\u4F60\u65E5\u5E38\u7684\u9ED8\u8BA4\u6D4F\u89C8\u5668\uFF08\u5305\u542B\u4F60\u7684\u4E66\u7B7E\u548C\u767B\u5F55\u6001\uFF09\uFF0C\u5982\u679C\u4F60\u7684 Chrome/Edge \u76EE\u524D\u6B63\u5904\u4E8E\u6253\u5F00\u72B6\u6001\uFF0C\u5B83\u4F1A\u62D2\u7EDD\u4F7F\u7528\u5E26\u6709\u8C03\u8BD5\u7AEF\u53E3\u7684\u65B0\u53C2\u6570\u542F\u52A8\u3002`));
|
|
272
|
+
console.error(pc.yellow(`\u{1F449} \u89E3\u51B3\u529E\u6CD5\uFF1A\u8BF7\u5148\u5B8C\u5168\u9000\u51FA\u5F53\u524D\u7684 Chrome \u6216 Edge \u6D4F\u89C8\u5668\uFF08\u5728 Mac \u4E0A\u6309 Cmd+Q\uFF0CWindows \u4E0A\u53F3\u952E\u4EFB\u52A1\u680F\u56FE\u6807\u9000\u51FA\uFF09\uFF0C\u7136\u540E\u518D\u91CD\u65B0\u8FD0\u884C\u547D\u4EE4\u3002`));
|
|
250
273
|
throw new Error("Browser connection failed.");
|
|
251
274
|
}
|
|
252
275
|
}
|
|
253
276
|
}
|
|
254
277
|
}
|
|
255
|
-
async function getPageTargetId(
|
|
256
|
-
const session = await
|
|
278
|
+
async function getPageTargetId(page2) {
|
|
279
|
+
const session = await page2.target().createCDPSession();
|
|
257
280
|
try {
|
|
258
281
|
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
259
282
|
return targetInfo.targetId;
|
|
@@ -262,9 +285,8 @@ async function getPageTargetId(page) {
|
|
|
262
285
|
});
|
|
263
286
|
}
|
|
264
287
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const pageTargets = targets.filter((t) => {
|
|
288
|
+
function getPageTargets(browser2) {
|
|
289
|
+
return browser2.targets().filter((t) => {
|
|
268
290
|
try {
|
|
269
291
|
const type = (typeof t.type === "function" ? t.type() : t.type) || "";
|
|
270
292
|
const url = (typeof t.url === "function" ? t.url() : t.url) || "";
|
|
@@ -273,23 +295,53 @@ async function getTargetPage(browser, tabid) {
|
|
|
273
295
|
return false;
|
|
274
296
|
}
|
|
275
297
|
});
|
|
298
|
+
}
|
|
299
|
+
function getTargetIdFromTarget(target) {
|
|
300
|
+
return typeof target._getTargetInfo === "function" ? target._getTargetInfo().targetId : target._targetId || target.targetId || "";
|
|
301
|
+
}
|
|
302
|
+
function findPageTargetByTabId(browser2, tabid2) {
|
|
303
|
+
const pageTargets = getPageTargets(browser2);
|
|
304
|
+
return pageTargets.find((t) => {
|
|
305
|
+
const tid = getTargetIdFromTarget(t);
|
|
306
|
+
return tid === tabid2 || tid.includes(tabid2);
|
|
307
|
+
}) ?? null;
|
|
308
|
+
}
|
|
309
|
+
async function activateTabById(browser2, tabid2) {
|
|
310
|
+
const target = findPageTargetByTabId(browser2, tabid2);
|
|
311
|
+
if (!target) {
|
|
312
|
+
throw new Error(`Tab with targetId "${tabid2}" not found.`);
|
|
313
|
+
}
|
|
314
|
+
const realTabId = getTargetIdFromTarget(target);
|
|
315
|
+
const pages = await browser2.pages();
|
|
316
|
+
let sessionPage = pages.find((p) => !p.url().startsWith("devtools://"));
|
|
317
|
+
if (!sessionPage && pages.length > 0) {
|
|
318
|
+
sessionPage = pages[0];
|
|
319
|
+
}
|
|
320
|
+
if (!sessionPage) {
|
|
321
|
+
sessionPage = await browser2.newPage();
|
|
322
|
+
}
|
|
323
|
+
const session = await sessionPage.createCDPSession();
|
|
324
|
+
try {
|
|
325
|
+
await session.send("Target.activateTarget", { targetId: realTabId });
|
|
326
|
+
} finally {
|
|
327
|
+
await session.detach().catch(() => {
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function getTargetPage(browser2, tabid2) {
|
|
332
|
+
const pageTargets = getPageTargets(browser2);
|
|
276
333
|
if (pageTargets.length === 0) {
|
|
277
|
-
const newPage = await
|
|
334
|
+
const newPage = await browser2.newPage();
|
|
278
335
|
await injectWebMCPPolyfillAndTools(newPage);
|
|
279
336
|
return newPage;
|
|
280
337
|
}
|
|
281
338
|
let targetPage = null;
|
|
282
|
-
if (
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
targetPage = await target.page();
|
|
287
|
-
break;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
if (!targetPage) {
|
|
291
|
-
throw new Error(`Tab with targetId "${tabid}" not found.`);
|
|
339
|
+
if (tabid2 !== void 0) {
|
|
340
|
+
const target = findPageTargetByTabId(browser2, tabid2);
|
|
341
|
+
if (!target) {
|
|
342
|
+
throw new Error(`Tab with targetId "${tabid2}" not found.`);
|
|
292
343
|
}
|
|
344
|
+
targetPage = await target.page();
|
|
293
345
|
} else {
|
|
294
346
|
try {
|
|
295
347
|
const urls = [
|
|
@@ -313,8 +365,7 @@ async function getTargetPage(browser, tabid) {
|
|
|
313
365
|
}
|
|
314
366
|
if (activeTargetId) {
|
|
315
367
|
for (const target of pageTargets) {
|
|
316
|
-
|
|
317
|
-
if (tid === activeTargetId) {
|
|
368
|
+
if (getTargetIdFromTarget(target) === activeTargetId) {
|
|
318
369
|
targetPage = await target.page();
|
|
319
370
|
break;
|
|
320
371
|
}
|
|
@@ -333,11 +384,11 @@ async function getTargetPage(browser, tabid) {
|
|
|
333
384
|
await injectWebMCPPolyfillAndTools(targetPage);
|
|
334
385
|
return targetPage;
|
|
335
386
|
}
|
|
336
|
-
async function injectIntoPage(
|
|
337
|
-
await injectWebMCPPolyfillAndTools(
|
|
387
|
+
async function injectIntoPage(page2) {
|
|
388
|
+
await injectWebMCPPolyfillAndTools(page2, true);
|
|
338
389
|
}
|
|
339
|
-
async function injectWebMCPPolyfillAndTools(
|
|
340
|
-
const polyfillReady = !force && await
|
|
390
|
+
async function injectWebMCPPolyfillAndTools(page2, force = false) {
|
|
391
|
+
const polyfillReady = !force && await page2.evaluate(() => {
|
|
341
392
|
return !!window.__webmcpcli_init;
|
|
342
393
|
}).catch(() => false);
|
|
343
394
|
if (!polyfillReady) {
|
|
@@ -348,19 +399,19 @@ async function injectWebMCPPolyfillAndTools(page, force = false) {
|
|
|
348
399
|
}
|
|
349
400
|
const scriptContent = fs.readFileSync(injectScriptPath, "utf-8");
|
|
350
401
|
try {
|
|
351
|
-
await
|
|
402
|
+
await page2.evaluate(scriptContent);
|
|
352
403
|
} catch (err) {
|
|
353
404
|
const msg = err instanceof Error ? err.message : String(err);
|
|
354
405
|
throw new Error("\u81EA\u52A8\u6CE8\u5165\u811A\u672C\u6267\u884C\u5931\u8D25: " + msg);
|
|
355
406
|
}
|
|
356
407
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
357
408
|
}
|
|
358
|
-
await injectDomainTools(
|
|
409
|
+
await injectDomainTools(page2);
|
|
359
410
|
}
|
|
360
|
-
async function injectDomainTools(
|
|
411
|
+
async function injectDomainTools(page2) {
|
|
361
412
|
let hostname;
|
|
362
413
|
try {
|
|
363
|
-
const url = new URL(
|
|
414
|
+
const url = new URL(page2.url());
|
|
364
415
|
hostname = url.hostname;
|
|
365
416
|
} catch {
|
|
366
417
|
return;
|
|
@@ -372,7 +423,7 @@ async function injectDomainTools(page) {
|
|
|
372
423
|
console.log(pc.cyan(`\u68C0\u6D4B\u5230\u57DF\u540D ${hostname} \u6709\u9884\u7F6E\u5DE5\u5177\uFF0C\u6B63\u5728\u6CE8\u5165...`));
|
|
373
424
|
const toolScript = fs.readFileSync(toolBundlePath, "utf-8");
|
|
374
425
|
try {
|
|
375
|
-
await
|
|
426
|
+
await page2.evaluate(toolScript);
|
|
376
427
|
console.log(pc.green(`\u5DF2\u4E3A ${hostname} \u6CE8\u5165\u9884\u7F6E\u5DE5\u5177`));
|
|
377
428
|
} catch (err) {
|
|
378
429
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -381,11 +432,12 @@ async function injectDomainTools(page) {
|
|
|
381
432
|
}
|
|
382
433
|
|
|
383
434
|
// src/commands/state.ts
|
|
384
|
-
async function stateCommand({ tabid }) {
|
|
385
|
-
const
|
|
435
|
+
async function stateCommand({ tabid: tabid2 }) {
|
|
436
|
+
const browser2 = await connectBrowser();
|
|
386
437
|
try {
|
|
387
|
-
const
|
|
388
|
-
|
|
438
|
+
const page2 = await getTargetPage(browser2, tabid2);
|
|
439
|
+
await injectIntoPage(page2);
|
|
440
|
+
const state = await page2.evaluate(async () => {
|
|
389
441
|
const url = document.URL;
|
|
390
442
|
const title = document.title;
|
|
391
443
|
const mcp = navigator.modelContextTesting || navigator.modelContext;
|
|
@@ -433,27 +485,29 @@ async function stateCommand({ tabid }) {
|
|
|
433
485
|
webmcpTools
|
|
434
486
|
};
|
|
435
487
|
});
|
|
436
|
-
const pages = await
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
488
|
+
const pages = await browser2.pages();
|
|
489
|
+
const tabs2 = await Promise.all(
|
|
490
|
+
pages.map(async (p) => {
|
|
491
|
+
const pUrl = p.url();
|
|
492
|
+
if (pUrl.startsWith("devtools://")) return null;
|
|
493
|
+
const pTitle = await Promise.race([
|
|
494
|
+
p.title().catch(() => "Unknown"),
|
|
495
|
+
new Promise((resolve) => setTimeout(() => resolve("Unknown"), 500))
|
|
496
|
+
]);
|
|
497
|
+
return {
|
|
498
|
+
// 使用真实的 Chrome target ID,而非数组下标
|
|
499
|
+
tabid: await getPageTargetId(p).catch(() => pUrl),
|
|
500
|
+
title: pTitle,
|
|
501
|
+
url: pUrl
|
|
502
|
+
};
|
|
503
|
+
})
|
|
504
|
+
);
|
|
451
505
|
return {
|
|
452
506
|
...state,
|
|
453
|
-
tabs:
|
|
507
|
+
tabs: tabs2.filter(Boolean)
|
|
454
508
|
};
|
|
455
509
|
} finally {
|
|
456
|
-
await
|
|
510
|
+
await browser2.disconnect();
|
|
457
511
|
}
|
|
458
512
|
}
|
|
459
513
|
|
|
@@ -461,17 +515,17 @@ async function stateCommand({ tabid }) {
|
|
|
461
515
|
async function runCommand({
|
|
462
516
|
toolName,
|
|
463
517
|
argsJson,
|
|
464
|
-
tabid
|
|
518
|
+
tabid: tabid2
|
|
465
519
|
}) {
|
|
466
|
-
const
|
|
520
|
+
const browser2 = await connectBrowser();
|
|
467
521
|
try {
|
|
468
|
-
const
|
|
522
|
+
const page2 = await getTargetPage(browser2, tabid2);
|
|
469
523
|
try {
|
|
470
524
|
JSON.parse(argsJson);
|
|
471
525
|
} catch (e) {
|
|
472
526
|
throw new Error(`\u53C2\u6570\u4E0D\u662F\u6709\u6548\u7684 JSON: ${e.message}`);
|
|
473
527
|
}
|
|
474
|
-
const
|
|
528
|
+
const result2 = await page2.evaluate(async (name, inputString) => {
|
|
475
529
|
const mcp = navigator.modelContextTesting || navigator.modelContext;
|
|
476
530
|
if (!mcp || typeof mcp.executeTool !== "function") {
|
|
477
531
|
throw new Error("\u5F53\u524D\u9875\u9762\u6CA1\u6709\u6CE8\u5165 WebMCP \u73AF\u5883 (navigator.modelContextTesting.executeTool \u672A\u627E\u5230)");
|
|
@@ -485,69 +539,216 @@ async function runCommand({
|
|
|
485
539
|
}
|
|
486
540
|
return res;
|
|
487
541
|
}, toolName, argsJson);
|
|
488
|
-
return
|
|
542
|
+
return result2;
|
|
489
543
|
} finally {
|
|
490
|
-
await
|
|
544
|
+
await browser2.disconnect();
|
|
491
545
|
}
|
|
492
546
|
}
|
|
493
547
|
|
|
494
|
-
// src/commands/
|
|
495
|
-
|
|
496
|
-
|
|
548
|
+
// src/commands/evaluate.ts
|
|
549
|
+
async function evaluateCommand({
|
|
550
|
+
jsScript,
|
|
551
|
+
tabid
|
|
552
|
+
}) {
|
|
497
553
|
const browser = await connectBrowser();
|
|
498
554
|
try {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const targets = browser.targets();
|
|
506
|
-
const pageTargets = targets.filter((t) => {
|
|
507
|
-
try {
|
|
508
|
-
const type = (typeof t.type === "function" ? t.type() : t.type) || "";
|
|
509
|
-
const urlStr = (typeof t.url === "function" ? t.url() : t.url) || "";
|
|
510
|
-
return type === "page" && !urlStr.startsWith("devtools://");
|
|
511
|
-
} catch {
|
|
512
|
-
return false;
|
|
555
|
+
const page = await getTargetPage(browser, tabid);
|
|
556
|
+
const result = await page.evaluate(async (script) => {
|
|
557
|
+
try {
|
|
558
|
+
const result = eval(script);
|
|
559
|
+
if (result instanceof Promise) {
|
|
560
|
+
return await result;
|
|
513
561
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return tid === tabid || tid.includes(tabid);
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
if (selectedTarget) {
|
|
525
|
-
console.log(pc2.yellow("openCommand: \u6B63\u5728\u5C06\u5339\u914D\u7684 target \u8F6C\u6362\u4E3A page..."));
|
|
526
|
-
page = await selectedTarget.page();
|
|
527
|
-
} else if (pageTargets.length > 0) {
|
|
528
|
-
console.log(pc2.yellow("openCommand: \u6B63\u5728\u5C06\u6700\u540E\u4E00\u4E2A target \u8F6C\u6362\u4E3A page..."));
|
|
529
|
-
page = await pageTargets[pageTargets.length - 1].page();
|
|
530
|
-
}
|
|
531
|
-
if (!page) {
|
|
532
|
-
console.log(pc2.yellow("openCommand: \u6CA1\u6709\u627E\u5230\u53EF\u7528 page\uFF0C\u6B63\u5728\u521B\u5EFA\u65B0\u9875\u9762..."));
|
|
533
|
-
page = await browser.newPage();
|
|
562
|
+
return result;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
return {
|
|
565
|
+
error: true,
|
|
566
|
+
message: error.message,
|
|
567
|
+
stack: error.stack
|
|
568
|
+
};
|
|
534
569
|
}
|
|
570
|
+
}, jsScript);
|
|
571
|
+
return result;
|
|
572
|
+
} finally {
|
|
573
|
+
await browser.disconnect();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// src/commands/clipboard.ts
|
|
578
|
+
import clipboard from "clipboardy";
|
|
579
|
+
async function setClipboard(content) {
|
|
580
|
+
try {
|
|
581
|
+
await clipboard.write(content);
|
|
582
|
+
} catch (error) {
|
|
583
|
+
throw new Error(`\u8BBE\u7F6E\u526A\u8D34\u677F\u5931\u8D25: ${error.message}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/commands/tabs.ts
|
|
588
|
+
function normalizeUrl(url) {
|
|
589
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
590
|
+
return "https://" + url;
|
|
591
|
+
}
|
|
592
|
+
return url;
|
|
593
|
+
}
|
|
594
|
+
async function tabsOpenCommand(url) {
|
|
595
|
+
const browser2 = await connectBrowser();
|
|
596
|
+
try {
|
|
597
|
+
const page2 = await browser2.newPage();
|
|
598
|
+
url = normalizeUrl(url);
|
|
599
|
+
await page2.goto(url, { waitUntil: "domcontentloaded" });
|
|
600
|
+
await injectIntoPage(page2);
|
|
601
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
602
|
+
return {
|
|
603
|
+
success: true,
|
|
604
|
+
tabid: await getPageTargetId(page2),
|
|
605
|
+
url: page2.url(),
|
|
606
|
+
title: await page2.title()
|
|
607
|
+
};
|
|
608
|
+
} finally {
|
|
609
|
+
await browser2.disconnect();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function tabsCloseCommand(tabid2) {
|
|
613
|
+
const browser2 = await connectBrowser();
|
|
614
|
+
try {
|
|
615
|
+
const target = findPageTargetByTabId(browser2, tabid2);
|
|
616
|
+
if (!target) {
|
|
617
|
+
throw new Error(`Tab with targetId "${tabid2}" not found.`);
|
|
535
618
|
}
|
|
536
|
-
|
|
537
|
-
|
|
619
|
+
const page2 = await target.page();
|
|
620
|
+
if (!page2) {
|
|
621
|
+
throw new Error(`Tab with targetId "${tabid2}" not found.`);
|
|
538
622
|
}
|
|
539
|
-
|
|
540
|
-
await
|
|
541
|
-
await injectIntoPage(page);
|
|
623
|
+
const closedTabid = await getPageTargetId(page2).catch(() => tabid2);
|
|
624
|
+
await page2.close();
|
|
542
625
|
return {
|
|
543
626
|
success: true,
|
|
544
|
-
|
|
545
|
-
title: await page.title()
|
|
627
|
+
tabid: closedTabid
|
|
546
628
|
};
|
|
547
629
|
} finally {
|
|
548
|
-
await
|
|
630
|
+
await browser2.disconnect();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async function tabsSwitchCommand(tabid2) {
|
|
634
|
+
const browser2 = await connectBrowser();
|
|
635
|
+
try {
|
|
636
|
+
await activateTabById(browser2, tabid2);
|
|
637
|
+
const page2 = await getTargetPage(browser2, tabid2);
|
|
638
|
+
return {
|
|
639
|
+
success: true,
|
|
640
|
+
tabid: await getPageTargetId(page2),
|
|
641
|
+
url: page2.url(),
|
|
642
|
+
title: await page2.title()
|
|
643
|
+
};
|
|
644
|
+
} finally {
|
|
645
|
+
await browser2.disconnect();
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async function tabsBackCommand(tabid2) {
|
|
649
|
+
const browser2 = await connectBrowser();
|
|
650
|
+
try {
|
|
651
|
+
const page2 = await getTargetPage(browser2, tabid2);
|
|
652
|
+
const response = await page2.goBack();
|
|
653
|
+
if (!response) {
|
|
654
|
+
return {
|
|
655
|
+
success: false,
|
|
656
|
+
error: "\u65E0\u6CD5\u540E\u9000\uFF1A\u5DF2\u5728\u5386\u53F2\u8BB0\u5F55\u8D77\u70B9",
|
|
657
|
+
tabid: await getPageTargetId(page2).catch(() => tabid2),
|
|
658
|
+
url: page2.url(),
|
|
659
|
+
title: await page2.title()
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
await injectIntoPage(page2);
|
|
663
|
+
return {
|
|
664
|
+
success: true,
|
|
665
|
+
tabid: await getPageTargetId(page2),
|
|
666
|
+
url: page2.url(),
|
|
667
|
+
title: await page2.title()
|
|
668
|
+
};
|
|
669
|
+
} finally {
|
|
670
|
+
await browser2.disconnect();
|
|
549
671
|
}
|
|
550
672
|
}
|
|
673
|
+
async function tabsForwardCommand(tabid2) {
|
|
674
|
+
const browser2 = await connectBrowser();
|
|
675
|
+
try {
|
|
676
|
+
const page2 = await getTargetPage(browser2, tabid2);
|
|
677
|
+
const response = await page2.goForward();
|
|
678
|
+
if (!response) {
|
|
679
|
+
return {
|
|
680
|
+
success: false,
|
|
681
|
+
error: "\u65E0\u6CD5\u524D\u8FDB\uFF1A\u5DF2\u5728\u5386\u53F2\u8BB0\u5F55\u672B\u5C3E",
|
|
682
|
+
tabid: await getPageTargetId(page2).catch(() => tabid2),
|
|
683
|
+
url: page2.url(),
|
|
684
|
+
title: await page2.title()
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
await injectIntoPage(page2);
|
|
688
|
+
return {
|
|
689
|
+
success: true,
|
|
690
|
+
tabid: await getPageTargetId(page2),
|
|
691
|
+
url: page2.url(),
|
|
692
|
+
title: await page2.title()
|
|
693
|
+
};
|
|
694
|
+
} finally {
|
|
695
|
+
await browser2.disconnect();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// package.json
|
|
700
|
+
var package_default = {
|
|
701
|
+
name: "@opentiny/webmcp-cli",
|
|
702
|
+
version: "0.0.1-alpha.3",
|
|
703
|
+
type: "module",
|
|
704
|
+
description: "WebMCP CLI for AI Agents to interact with browser via Puppeteer",
|
|
705
|
+
main: "dist/index.js",
|
|
706
|
+
types: "dist/index.d.ts",
|
|
707
|
+
bin: {
|
|
708
|
+
"webmcp-cli": "./dist/bin.js"
|
|
709
|
+
},
|
|
710
|
+
files: [
|
|
711
|
+
"dist",
|
|
712
|
+
"README.md"
|
|
713
|
+
],
|
|
714
|
+
scripts: {
|
|
715
|
+
dev: "tsup --watch",
|
|
716
|
+
"build:inject": "node scripts/build-inject.mjs",
|
|
717
|
+
build: "tsup && pnpm run build:inject",
|
|
718
|
+
prepublishOnly: "pnpm run build",
|
|
719
|
+
"link:global": "npm install -g .",
|
|
720
|
+
start: "node dist/bin.js"
|
|
721
|
+
},
|
|
722
|
+
keywords: [
|
|
723
|
+
"chrome",
|
|
724
|
+
"puppeteer",
|
|
725
|
+
"cli"
|
|
726
|
+
],
|
|
727
|
+
license: "MIT",
|
|
728
|
+
author: "OpenTiny Team",
|
|
729
|
+
engines: {
|
|
730
|
+
node: ">=16"
|
|
731
|
+
},
|
|
732
|
+
publishConfig: {
|
|
733
|
+
access: "public"
|
|
734
|
+
},
|
|
735
|
+
dependencies: {
|
|
736
|
+
"@mcp-b/webmcp-polyfill": "catalog:",
|
|
737
|
+
"@opentiny/next-sdk": "workspace:^",
|
|
738
|
+
"@page-agent/page-controller": "catalog:",
|
|
739
|
+
clipboardy: "^5.3.1",
|
|
740
|
+
commander: "^11.1.0",
|
|
741
|
+
picocolors: "^1.0.0",
|
|
742
|
+
"puppeteer-core": "^22.0.0"
|
|
743
|
+
},
|
|
744
|
+
devDependencies: {
|
|
745
|
+
"@types/node": "^20.0.0",
|
|
746
|
+
esbuild: "^0.25.0",
|
|
747
|
+
tsup: "^8.0.2",
|
|
748
|
+
tsx: "^4.0.0",
|
|
749
|
+
typescript: "^5.3.3"
|
|
750
|
+
}
|
|
751
|
+
};
|
|
551
752
|
|
|
552
753
|
// src/bin.ts
|
|
553
754
|
var program = new Command();
|
|
@@ -555,7 +756,12 @@ function parseTabId(id) {
|
|
|
555
756
|
if (!id) return void 0;
|
|
556
757
|
return id;
|
|
557
758
|
}
|
|
558
|
-
|
|
759
|
+
function handleCommandError(error, commandName) {
|
|
760
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
761
|
+
console.error(pc2.red(`Error executing ${commandName} command: ${msg}`));
|
|
762
|
+
process.exit(1);
|
|
763
|
+
}
|
|
764
|
+
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) => {
|
|
559
765
|
const opts = thisCommand.opts();
|
|
560
766
|
if (opts.workspace) {
|
|
561
767
|
process.env.WEBMCP_WORKSPACE = opts.workspace;
|
|
@@ -563,39 +769,82 @@ program.name("webmcp-cli").description("WebMCP CLI for interacting with browser
|
|
|
563
769
|
});
|
|
564
770
|
program.command("state").description("\u83B7\u53D6\u6D4F\u89C8\u5668\u5F53\u524D\u9875\u7B7E\u6216\u6307\u5B9A\u9875\u7B7E\u7684\u72B6\u6001\uFF08\u5185\u5BB9\u3001\u6240\u6709\u9875\u7B7E\u5217\u8868\u3001\u53EF\u7528 WebMCP \u5DE5\u5177\u5217\u8868\uFF09").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (options) => {
|
|
565
771
|
try {
|
|
566
|
-
const
|
|
567
|
-
console.log(JSON.stringify(
|
|
772
|
+
const result2 = await stateCommand({ tabid: parseTabId(options.tabid) });
|
|
773
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
568
774
|
} catch (error) {
|
|
569
|
-
|
|
570
|
-
console.error(pc3.red(`Error executing state command: ${msg}`));
|
|
571
|
-
process.exit(1);
|
|
775
|
+
handleCommandError(error, "state");
|
|
572
776
|
}
|
|
573
777
|
});
|
|
574
778
|
program.command("run <toolName> <argsJson>").description("\u5411\u6307\u5B9A\u9875\u7B7E\u8C03\u7528\u6307\u5B9A\u7684 WebMCP \u5DE5\u5177\u6267\u884C\u64CD\u4F5C").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (toolName, argsJson, options) => {
|
|
575
779
|
try {
|
|
576
|
-
const
|
|
780
|
+
const result2 = await runCommand({
|
|
577
781
|
toolName,
|
|
578
782
|
argsJson,
|
|
579
783
|
tabid: parseTabId(options.tabid)
|
|
580
784
|
});
|
|
581
|
-
console.log(JSON.stringify(
|
|
785
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
582
786
|
} catch (error) {
|
|
583
|
-
|
|
584
|
-
console.error(pc3.red(`Error executing run command: ${msg}`));
|
|
585
|
-
process.exit(1);
|
|
787
|
+
handleCommandError(error, "run");
|
|
586
788
|
}
|
|
587
789
|
});
|
|
588
|
-
program.command("
|
|
790
|
+
program.command("evaluate <jsScript>").description("\u5411\u5F53\u524D\u7F51\u9875\u6216\u6307\u5B9A\u9875\u7B7E\u6CE8\u5165\u5E76\u6267\u884C JavaScript \u4EE3\u7801").option("-t, --tabid <id>", "\u6307\u5B9A\u9875\u7B7E\u7684 ID").action(async (jsScript2, options) => {
|
|
589
791
|
try {
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
792
|
+
const result2 = await evaluateCommand({
|
|
793
|
+
jsScript: jsScript2,
|
|
794
|
+
tabid: parseTabId(options.tabid)
|
|
593
795
|
});
|
|
594
|
-
console.log(JSON.stringify(
|
|
796
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
797
|
+
} catch (error) {
|
|
798
|
+
handleCommandError(error, "evaluate");
|
|
799
|
+
}
|
|
800
|
+
});
|
|
801
|
+
program.command("clipboard <content>").description("\u5C06\u5185\u5BB9\u8BBE\u7F6E\u5230\u7CFB\u7EDF\u526A\u8D34\u677F").action(async (content) => {
|
|
802
|
+
try {
|
|
803
|
+
await setClipboard(content);
|
|
804
|
+
console.log(JSON.stringify({ success: true, message: "\u5185\u5BB9\u5DF2\u8BBE\u7F6E\u5230\u7CFB\u7EDF\u526A\u8D34\u677F" }, null, 2));
|
|
805
|
+
} catch (error) {
|
|
806
|
+
handleCommandError(error, "clipboard");
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
var tabs = program.command("tabs").description("\u7BA1\u7406\u6D4F\u89C8\u5668\u6807\u7B7E\u9875");
|
|
810
|
+
tabs.command("open <url>").description("\u6253\u5F00\u65B0\u7F51\u9875").action(async (url) => {
|
|
811
|
+
try {
|
|
812
|
+
const result2 = await tabsOpenCommand(url);
|
|
813
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
814
|
+
} catch (error) {
|
|
815
|
+
handleCommandError(error, "tabs open");
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
tabs.command("close <tabid>").description("\u5173\u95ED\u6307\u5B9A tabid \u7684\u6807\u7B7E\u9875").action(async (tabid2) => {
|
|
819
|
+
try {
|
|
820
|
+
const result2 = await tabsCloseCommand(tabid2);
|
|
821
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
822
|
+
} catch (error) {
|
|
823
|
+
handleCommandError(error, "tabs close");
|
|
824
|
+
}
|
|
825
|
+
});
|
|
826
|
+
tabs.command("switch <tabid>").description("\u6FC0\u6D3B\u5E76\u5207\u6362\u5230\u6307\u5B9A tabid \u7684\u6807\u7B7E\u9875").action(async (tabid2) => {
|
|
827
|
+
try {
|
|
828
|
+
const result2 = await tabsSwitchCommand(tabid2);
|
|
829
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
830
|
+
} catch (error) {
|
|
831
|
+
handleCommandError(error, "tabs switch");
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
tabs.command("back [tabid]").description("\u5C06\u5F53\u524D\u6216\u6307\u5B9A\u6807\u7B7E\u9875\u5BFC\u822A\u540E\u9000\u4E00\u6B65").action(async (tabid2) => {
|
|
835
|
+
try {
|
|
836
|
+
const result2 = await tabsBackCommand(parseTabId(tabid2));
|
|
837
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
838
|
+
} catch (error) {
|
|
839
|
+
handleCommandError(error, "tabs back");
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
tabs.command("forward [tabid]").description("\u5C06\u5F53\u524D\u6216\u6307\u5B9A\u6807\u7B7E\u9875\u5BFC\u822A\u524D\u8FDB\u4E00\u6B65").action(async (tabid2) => {
|
|
843
|
+
try {
|
|
844
|
+
const result2 = await tabsForwardCommand(parseTabId(tabid2));
|
|
845
|
+
console.log(JSON.stringify(result2, null, 2));
|
|
595
846
|
} catch (error) {
|
|
596
|
-
|
|
597
|
-
console.error(pc3.red(`Error executing open command: ${msg}`));
|
|
598
|
-
process.exit(1);
|
|
847
|
+
handleCommandError(error, "tabs forward");
|
|
599
848
|
}
|
|
600
849
|
});
|
|
601
850
|
program.parse(process.argv);
|