@xbrowser/cli 1.0.0 → 1.0.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.
Files changed (48) hide show
  1. package/README.md +17 -26
  2. package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
  3. package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
  4. package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
  5. package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
  6. package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
  9. package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
  10. package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
  11. package/dist/chunk-AMI64BSD.js +268 -0
  12. package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
  13. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  14. package/dist/chunk-GDKLH7ZY.js +8 -0
  15. package/dist/chunk-KFQGP6VL.js +33 -0
  16. package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
  17. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  18. package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
  19. package/dist/chunk-OZKD3W4X.js +417 -0
  20. package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
  21. package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
  22. package/dist/chunk-WJRE55TN.js +83 -0
  23. package/dist/cli.js +1558 -1122
  24. package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
  25. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  26. package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
  27. package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
  28. package/dist/daemon-main.js +2296 -1722
  29. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  30. package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
  31. package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
  32. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  33. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  34. package/dist/index.d.ts +166 -109
  35. package/dist/index.js +2668 -1742
  36. package/dist/launcher-L2JNDB2H.js +20 -0
  37. package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
  38. package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
  39. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  40. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  41. package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
  42. package/dist/session-recorder-RTDGURIJ.js +8 -0
  43. package/dist/session-recorder-YI7YYM36.js +7 -0
  44. package/dist/session-replayer-MY27H4DX.js +276 -0
  45. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  46. package/package.json +5 -4
  47. package/dist/screenshot-CWAWMXVA.js +0 -28
  48. package/dist/session-recorder-MA75PKTQ.js +0 -7
@@ -0,0 +1,417 @@
1
+ import {
2
+ __esm,
3
+ __export,
4
+ __require
5
+ } from "./chunk-KFQGP6VL.js";
6
+
7
+ // src/recorder/site-knowledge.ts
8
+ var site_knowledge_exports = {};
9
+ __export(site_knowledge_exports, {
10
+ addKnownIssue: () => addKnownIssue,
11
+ getKnowledgeDir: () => getKnowledgeDir,
12
+ getKnowledgePath: () => getKnowledgePath,
13
+ listSiteKnowledge: () => listSiteKnowledge,
14
+ readSiteKnowledge: () => readSiteKnowledge,
15
+ readSiteKnowledgeMarkdown: () => readSiteKnowledgeMarkdown,
16
+ toMarkdown: () => toMarkdown,
17
+ updateSiteKnowledge: () => updateSiteKnowledge
18
+ });
19
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
20
+ import { join } from "path";
21
+ import { homedir } from "os";
22
+ function getKnowledgeDir() {
23
+ return join(homedir(), ".xbrowser", "knowledge");
24
+ }
25
+ function getKnowledgePath(domain, ext) {
26
+ return join(getKnowledgeDir(), `${domain}.${ext}`);
27
+ }
28
+ function extractDomain(url) {
29
+ try {
30
+ const u = new URL(url);
31
+ return u.hostname.replace(/^www\./, "");
32
+ } catch {
33
+ return "unknown";
34
+ }
35
+ }
36
+ function normalizePath(url) {
37
+ try {
38
+ const u = new URL(url);
39
+ return u.pathname;
40
+ } catch {
41
+ return url;
42
+ }
43
+ }
44
+ function extractSelectors(actions, pageUrl) {
45
+ const seen = /* @__PURE__ */ new Map();
46
+ const now = (/* @__PURE__ */ new Date()).toISOString();
47
+ for (const action of actions) {
48
+ if (normalizePath(action.url) !== normalizePath(pageUrl)) continue;
49
+ const el = action.element;
50
+ if (!el || !el.selector) continue;
51
+ const key = el.selector;
52
+ const existing = seen.get(key);
53
+ if (existing) {
54
+ existing.timesSeen++;
55
+ existing.lastSeen = now;
56
+ } else {
57
+ const description = buildDescription(el, action);
58
+ seen.set(key, {
59
+ selector: key,
60
+ tag: el.tag || "unknown",
61
+ description,
62
+ actionType: action.type,
63
+ role: el.role,
64
+ text: el.text?.substring(0, 60),
65
+ confidence: el.confidence || "medium",
66
+ lastSeen: now,
67
+ timesSeen: 1,
68
+ status: "active"
69
+ });
70
+ }
71
+ }
72
+ return Array.from(seen.values()).sort((a, b) => b.timesSeen - a.timesSeen);
73
+ }
74
+ function buildDescription(el, action) {
75
+ const parts = [];
76
+ if (el.text) parts.push(`"${el.text}"`);
77
+ if (el.placeholder) parts.push(`placeholder="${el.placeholder}"`);
78
+ if (el.ariaLabel) parts.push(`aria-label="${el.ariaLabel}"`);
79
+ if (el.role) parts.push(`role=${el.role}`);
80
+ if (el.type) parts.push(`type=${el.type}`);
81
+ const actionDesc = {
82
+ click: "clicked",
83
+ input: `filled with "${action.value?.substring(0, 30)}"`,
84
+ change: "changed",
85
+ submit: "submitted form",
86
+ dblclick: "double-clicked",
87
+ contextmenu: "right-clicked",
88
+ hover: "hovered over",
89
+ focus: "focused"
90
+ };
91
+ const verb = actionDesc[action.type] || action.type;
92
+ const base = parts.length > 0 ? parts.join(", ") : el.tag || "element";
93
+ return `${base} \u2014 ${verb}`;
94
+ }
95
+ function extractForms(actions, pageUrl) {
96
+ const forms = /* @__PURE__ */ new Map();
97
+ for (const action of actions) {
98
+ if (normalizePath(action.url) !== normalizePath(pageUrl)) continue;
99
+ const el = action.element;
100
+ if (!el) continue;
101
+ if (el.tag === "input" || el.tag === "textarea" || el.tag === "select") {
102
+ const formKey = "main";
103
+ const form = forms.get(formKey) || {
104
+ name: "Main Form",
105
+ action: pageUrl,
106
+ fields: []
107
+ };
108
+ if (!form.fields.some((f) => f.selector === el.selector)) {
109
+ form.fields.push({
110
+ selector: el.selector || "",
111
+ tag: el.tag,
112
+ label: el.ariaLabel || el.placeholder || el.text || el.selector || "",
113
+ inputType: el.type || el.tag,
114
+ placeholder: el.placeholder
115
+ });
116
+ }
117
+ forms.set(formKey, form);
118
+ }
119
+ if (action.type === "submit" || action.type === "click" && el.tag === "button" && el.text) {
120
+ const form = forms.get("main");
121
+ if (form && !form.submitSelector) {
122
+ form.submitSelector = el.selector;
123
+ }
124
+ }
125
+ }
126
+ return Array.from(forms.values());
127
+ }
128
+ function extractNavLinks(actions) {
129
+ const links = [];
130
+ const seen = /* @__PURE__ */ new Set();
131
+ for (const action of actions) {
132
+ if (action.type !== "click" && action.type !== "navigation") continue;
133
+ const el = action.element;
134
+ if (!el || el.tag !== "a" || !el.text) continue;
135
+ const href = el.href || action.url;
136
+ if (!href || seen.has(href)) continue;
137
+ seen.add(href);
138
+ links.push({
139
+ text: el.text.substring(0, 40),
140
+ href,
141
+ selector: el.selector || ""
142
+ });
143
+ }
144
+ return links;
145
+ }
146
+ function extractApiEndpoints(network, existingEndpoints) {
147
+ const endpoints = {};
148
+ const now = (/* @__PURE__ */ new Date()).toISOString();
149
+ if (existingEndpoints) {
150
+ for (const [key, ep] of Object.entries(existingEndpoints)) {
151
+ endpoints[key] = ep;
152
+ }
153
+ }
154
+ for (const entry of network) {
155
+ if (!entry.url.includes("/api/") && !entry.url.includes("/v1/") && !entry.url.includes("/v2/") && entry.contentType && !entry.contentType.includes("json") && !entry.contentType.includes("text/")) continue;
156
+ if (["image", "stylesheet", "font", "manifest"].includes(entry.resourceType)) continue;
157
+ let path;
158
+ try {
159
+ path = new URL(entry.url).pathname;
160
+ } catch {
161
+ continue;
162
+ }
163
+ const method = entry.method;
164
+ const key = `${method} ${path}`;
165
+ if (endpoints[key]) {
166
+ endpoints[key].timesSeen++;
167
+ endpoints[key].lastSeen = now;
168
+ } else {
169
+ let params = [];
170
+ if (entry.requestBody && typeof entry.requestBody === "object") {
171
+ params = Object.keys(entry.requestBody).slice(0, 10);
172
+ }
173
+ let responseFields = [];
174
+ if (entry.responseBody && typeof entry.responseBody === "object") {
175
+ const resp = entry.responseBody;
176
+ responseFields = Object.keys(resp).slice(0, 10);
177
+ if (resp.data && typeof resp.data === "object") {
178
+ const dataKeys = Object.keys(resp.data).slice(0, 10);
179
+ responseFields = [...responseFields, ...dataKeys.map((k) => `data.${k}`)];
180
+ }
181
+ }
182
+ endpoints[key] = {
183
+ method,
184
+ url: entry.url.substring(0, 200),
185
+ path,
186
+ params,
187
+ responseFields,
188
+ lastSeen: now,
189
+ timesSeen: 1
190
+ };
191
+ }
192
+ }
193
+ return endpoints;
194
+ }
195
+ function mergePages(existing, newPages) {
196
+ const merged = { ...existing };
197
+ const now = (/* @__PURE__ */ new Date()).toISOString();
198
+ for (const [path, newPage] of Object.entries(newPages)) {
199
+ if (merged[path]) {
200
+ const old = merged[path];
201
+ const selectorMap = /* @__PURE__ */ new Map();
202
+ for (const sel of old.selectors) selectorMap.set(sel.selector, sel);
203
+ for (const sel of newPage.selectors) {
204
+ const existing2 = selectorMap.get(sel.selector);
205
+ if (existing2) {
206
+ existing2.timesSeen += sel.timesSeen;
207
+ existing2.lastSeen = now;
208
+ existing2.status = "active";
209
+ } else {
210
+ selectorMap.set(sel.selector, sel);
211
+ }
212
+ }
213
+ const newSelectorSet = new Set(newPage.selectors.map((s) => s.selector));
214
+ for (const sel of selectorMap.values()) {
215
+ if (!newSelectorSet.has(sel.selector) && sel.timesSeen > 0) {
216
+ }
217
+ }
218
+ merged[path] = {
219
+ ...newPage,
220
+ selectors: Array.from(selectorMap.values()).sort((a, b) => b.timesSeen - a.timesSeen),
221
+ forms: newPage.forms.length > 0 ? newPage.forms : old.forms,
222
+ navigationLinks: [.../* @__PURE__ */ new Set([...old.navigationLinks, ...newPage.navigationLinks])].slice(0, 50),
223
+ lastVisited: now
224
+ };
225
+ } else {
226
+ merged[path] = newPage;
227
+ }
228
+ }
229
+ return merged;
230
+ }
231
+ function updateSiteKnowledge(data) {
232
+ const domain = extractDomain(data.startUrl);
233
+ const jsonPath = getKnowledgePath(domain, "json");
234
+ let existing = null;
235
+ if (existsSync(jsonPath)) {
236
+ try {
237
+ existing = JSON.parse(readFileSync(jsonPath, "utf-8"));
238
+ } catch {
239
+ existing = null;
240
+ }
241
+ }
242
+ const pageMap = /* @__PURE__ */ new Map();
243
+ for (const action of data.actions) {
244
+ const path = normalizePath(action.url);
245
+ if (!pageMap.has(path)) pageMap.set(path, []);
246
+ pageMap.get(path).push(action);
247
+ }
248
+ const newPages = {};
249
+ for (const [path, actions] of pageMap) {
250
+ const fullUrl = actions[0]?.url || data.startUrl;
251
+ newPages[path] = {
252
+ url: fullUrl,
253
+ title: actions[0]?.pageTitle || "",
254
+ selectors: extractSelectors(data.actions, fullUrl),
255
+ forms: extractForms(data.actions, fullUrl),
256
+ navigationLinks: extractNavLinks(actions),
257
+ lastVisited: (/* @__PURE__ */ new Date()).toISOString()
258
+ };
259
+ }
260
+ const apiEndpoints = extractApiEndpoints(data.network, existing?.apiEndpoints);
261
+ const pages = existing ? mergePages(existing.pages, newPages) : newPages;
262
+ const knowledge = {
263
+ domain,
264
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString(),
265
+ recordingCount: (existing?.recordingCount || 0) + 1,
266
+ pages,
267
+ apiEndpoints,
268
+ knownIssues: existing?.knownIssues || [],
269
+ generatedBy: "xbrowser-recorder"
270
+ };
271
+ mkdirSync(getKnowledgeDir(), { recursive: true });
272
+ writeFileSync(jsonPath, JSON.stringify(knowledge, null, 2), "utf-8");
273
+ writeFileSync(getKnowledgePath(domain, "md"), toMarkdown(knowledge), "utf-8");
274
+ return knowledge;
275
+ }
276
+ function toMarkdown(kb) {
277
+ const lines = [];
278
+ lines.push(`# Site Knowledge: ${kb.domain}`);
279
+ lines.push("");
280
+ lines.push("> **Auto-generated by xbrowser recorder. This document is for LLM consumption.**");
281
+ lines.push("> Use these selectors when writing automation scripts for this site.");
282
+ lines.push(`> Updated: ${kb.lastUpdated} | Recordings: ${kb.recordingCount}`);
283
+ lines.push("");
284
+ lines.push("## Pages");
285
+ lines.push("");
286
+ for (const page of Object.values(kb.pages)) {
287
+ lines.push(`### ${page.url}`);
288
+ lines.push(`- **Path**: ${normalizePath(page.url)}`);
289
+ if (page.title) lines.push(`- **Title**: ${page.title}`);
290
+ lines.push(`- **Last Visited**: ${page.lastVisited}`);
291
+ lines.push("");
292
+ if (page.selectors.length > 0) {
293
+ lines.push("#### Selectors");
294
+ lines.push("");
295
+ lines.push("| Selector | Tag | Action | Description | Confidence | Seen |");
296
+ lines.push("|----------|-----|--------|-------------|------------|------|");
297
+ for (const sel of page.selectors) {
298
+ const status = sel.status === "deprecated" ? " \u26A0\uFE0FDEPRECATED" : "";
299
+ lines.push(
300
+ `| \`${sel.selector}\` | ${sel.tag} | ${sel.actionType} | ${sel.description} | ${sel.confidence} | ${sel.timesSeen}x${status} |`
301
+ );
302
+ }
303
+ lines.push("");
304
+ }
305
+ if (page.forms.length > 0) {
306
+ lines.push("#### Forms");
307
+ lines.push("");
308
+ for (const form of page.forms) {
309
+ lines.push(`- **${form.name}** (${form.action})`);
310
+ for (const field of form.fields) {
311
+ const parts = [field.tag, field.inputType];
312
+ if (field.placeholder) parts.push(`placeholder="${field.placeholder}"`);
313
+ lines.push(` - \`${field.selector}\` \u2192 ${field.label} (${parts.join(", ")})`);
314
+ }
315
+ if (form.submitSelector) {
316
+ lines.push(` - Submit: \`${form.submitSelector}\``);
317
+ }
318
+ }
319
+ lines.push("");
320
+ }
321
+ if (page.navigationLinks.length > 0) {
322
+ lines.push("#### Navigation Links");
323
+ lines.push("");
324
+ for (const link of page.navigationLinks.slice(0, 20)) {
325
+ lines.push(`- [${link.text}](${link.href}) \u2192 \`${link.selector}\``);
326
+ }
327
+ lines.push("");
328
+ }
329
+ }
330
+ const endpoints = Object.values(kb.apiEndpoints);
331
+ if (endpoints.length > 0) {
332
+ lines.push("## API Endpoints");
333
+ lines.push("");
334
+ lines.push("| Method | Path | Params | Response Fields | Frequency |");
335
+ lines.push("|--------|------|--------|-----------------|-----------|");
336
+ for (const ep of endpoints.sort((a, b) => b.timesSeen - a.timesSeen)) {
337
+ const params = ep.params.length > 0 ? ep.params.join(", ") : "-";
338
+ const respFields = ep.responseFields.length > 0 ? ep.responseFields.slice(0, 5).join(", ") : "-";
339
+ lines.push(`| ${ep.method} | ${ep.path} | ${params} | ${respFields} | ${ep.timesSeen}x |`);
340
+ }
341
+ lines.push("");
342
+ }
343
+ if (kb.knownIssues.length > 0) {
344
+ lines.push("## Known Issues");
345
+ lines.push("");
346
+ for (const issue of kb.knownIssues) {
347
+ lines.push(`- ${issue}`);
348
+ }
349
+ lines.push("");
350
+ }
351
+ lines.push("---");
352
+ lines.push("");
353
+ lines.push("## How to Use This Document");
354
+ lines.push("");
355
+ lines.push("When writing automation scripts for this site:");
356
+ lines.push("1. Use the selectors from the **Selectors** tables above");
357
+ lines.push("2. Prefer selectors with **high confidence** and **higher Seen count**");
358
+ lines.push("3. For form filling, follow the **Forms** structure");
359
+ lines.push("4. For API interactions, reference the **API Endpoints** table");
360
+ lines.push("5. If a selector fails, it may be **deprecated** \u2014 check for alternative selectors");
361
+ lines.push("");
362
+ return lines.join("\n");
363
+ }
364
+ function readSiteKnowledge(domain) {
365
+ const path = getKnowledgePath(domain, "json");
366
+ if (!existsSync(path)) return null;
367
+ try {
368
+ return JSON.parse(readFileSync(path, "utf-8"));
369
+ } catch {
370
+ return null;
371
+ }
372
+ }
373
+ function readSiteKnowledgeMarkdown(domain) {
374
+ const path = getKnowledgePath(domain, "md");
375
+ if (!existsSync(path)) return null;
376
+ try {
377
+ return readFileSync(path, "utf-8");
378
+ } catch {
379
+ return null;
380
+ }
381
+ }
382
+ function listSiteKnowledge() {
383
+ const dir = getKnowledgeDir();
384
+ if (!existsSync(dir)) return [];
385
+ try {
386
+ const files = __require("fs").readdirSync(dir);
387
+ return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(".json", ""));
388
+ } catch {
389
+ return [];
390
+ }
391
+ }
392
+ function addKnownIssue(domain, issue) {
393
+ const kb = readSiteKnowledge(domain);
394
+ if (!kb) return;
395
+ const dated = `[${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}] ${issue}`;
396
+ kb.knownIssues.push(dated);
397
+ kb.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
398
+ writeFileSync(getKnowledgePath(domain, "json"), JSON.stringify(kb, null, 2), "utf-8");
399
+ writeFileSync(getKnowledgePath(domain, "md"), toMarkdown(kb), "utf-8");
400
+ }
401
+ var init_site_knowledge = __esm({
402
+ "src/recorder/site-knowledge.ts"() {
403
+ }
404
+ });
405
+
406
+ export {
407
+ getKnowledgeDir,
408
+ getKnowledgePath,
409
+ updateSiteKnowledge,
410
+ toMarkdown,
411
+ readSiteKnowledge,
412
+ readSiteKnowledgeMarkdown,
413
+ listSiteKnowledge,
414
+ addKnownIssue,
415
+ site_knowledge_exports,
416
+ init_site_knowledge
417
+ };
@@ -1,6 +1,9 @@
1
+ import {
2
+ errMsg
3
+ } from "./chunk-GDKLH7ZY.js";
1
4
  import {
2
5
  __require
3
- } from "./chunk-3RG5ZIWI.js";
6
+ } from "./chunk-KFQGP6VL.js";
4
7
 
5
8
  // src/cdp-driver/launcher.ts
6
9
  import { spawn } from "child_process";
@@ -71,7 +74,16 @@ async function launchChrome(options = {}) {
71
74
  const chromePath = executablePath ?? findChrome();
72
75
  if (!chromePath) {
73
76
  throw new Error(
74
- "Chrome/Chromium not found. Set executablePath or install Chrome to a default location."
77
+ [
78
+ "Chrome/Chromium not found.",
79
+ "",
80
+ "\u63A8\u8350\uFF1A\u7528 cdp-tunnel \u590D\u7528\u4F60\u5DF2\u6709\u7684 Chrome\uFF08\u542B\u767B\u5F55\u6001\u3001\u53CD\u722C\u53CB\u597D\uFF09",
81
+ " npx cdp-tunnel setup # \u96F6\u5B89\u88C5\u4E00\u952E\u542F\u52A8\u4EE3\u7406 + \u52A0\u8F7D Chrome \u6269\u5C55",
82
+ " xbrowser goto https://example.com --cdp http://localhost:9221",
83
+ "",
84
+ "\u6216\u6307\u5B9A Chrome \u8DEF\u5F84\uFF1A",
85
+ ' xbrowser config set browser.executablePath "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"'
86
+ ].join("\n")
75
87
  );
76
88
  }
77
89
  const port = await findFreePort();
@@ -84,7 +96,8 @@ async function launchChrome(options = {}) {
84
96
  "--disable-background-timer-throttling",
85
97
  "--disable-backgrounding-occluded-windows",
86
98
  "--disable-renderer-backgrounding",
87
- "--disable-features=Translate"
99
+ "--disable-features=Translate",
100
+ "--disable-popup-blocking"
88
101
  ];
89
102
  if (headless) {
90
103
  allArgs.push("--headless", "--hide-scrollbars", "--mute-audio");
@@ -136,7 +149,7 @@ async function launchChrome(options = {}) {
136
149
  } catch (err) {
137
150
  const stderr = stderrLines.slice(-20).join("\n");
138
151
  const exitInfo = child.exitCode !== null ? ` (exit code: ${child.exitCode})` : " (still running)";
139
- throw new Error(`${err.message}${exitInfo}
152
+ throw new Error(`${errMsg(err)}${exitInfo}
140
153
  Chrome stderr:
141
154
  ${stderr || "(empty)"}`);
142
155
  }