@wordbricks/playwright-mcp 0.1.19 → 0.1.22

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 (88) hide show
  1. package/README.md +54 -44
  2. package/cli-wrapper.js +15 -14
  3. package/cli.js +1 -1
  4. package/config.d.ts +11 -6
  5. package/index.d.ts +7 -5
  6. package/index.js +1 -1
  7. package/lib/browserContextFactory.js +131 -58
  8. package/lib/browserServerBackend.js +14 -12
  9. package/lib/config.js +60 -46
  10. package/lib/context.js +41 -39
  11. package/lib/extension/cdpRelay.js +67 -61
  12. package/lib/extension/extensionContextFactory.js +10 -10
  13. package/lib/frameworkPatterns.js +21 -21
  14. package/lib/hooks/antiBotDetectionHook.js +178 -0
  15. package/lib/hooks/core.js +11 -10
  16. package/lib/hooks/eventConsumer.js +29 -16
  17. package/lib/hooks/events.js +3 -3
  18. package/lib/hooks/formatToolCallEvent.js +3 -7
  19. package/lib/hooks/frameworkStateHook.js +40 -40
  20. package/lib/hooks/grouping.js +3 -3
  21. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  22. package/lib/hooks/networkFilters.js +24 -15
  23. package/lib/hooks/networkSetup.js +11 -6
  24. package/lib/hooks/networkTrackingHook.js +31 -19
  25. package/lib/hooks/pageHeightHook.js +9 -9
  26. package/lib/hooks/registry.js +18 -16
  27. package/lib/hooks/requireTabHook.js +3 -3
  28. package/lib/hooks/schema.js +44 -32
  29. package/lib/hooks/waitHook.js +7 -7
  30. package/lib/index.js +12 -10
  31. package/lib/mcp/inProcessTransport.js +3 -4
  32. package/lib/mcp/proxyBackend.js +43 -28
  33. package/lib/mcp/server.js +24 -19
  34. package/lib/mcp/tool.js +14 -8
  35. package/lib/mcp/transport.js +60 -53
  36. package/lib/playwrightTransformer.js +129 -106
  37. package/lib/program.js +54 -52
  38. package/lib/response.js +36 -30
  39. package/lib/sessionLog.js +19 -17
  40. package/lib/tab.js +41 -39
  41. package/lib/tools/common.js +19 -19
  42. package/lib/tools/console.js +11 -11
  43. package/lib/tools/dialogs.js +18 -15
  44. package/lib/tools/evaluate.js +26 -17
  45. package/lib/tools/extractFrameworkState.js +48 -37
  46. package/lib/tools/files.js +17 -14
  47. package/lib/tools/form.js +32 -23
  48. package/lib/tools/getSnapshot.js +14 -15
  49. package/lib/tools/getVisibleHtml.js +33 -17
  50. package/lib/tools/install.js +20 -20
  51. package/lib/tools/keyboard.js +29 -24
  52. package/lib/tools/mouse.js +29 -31
  53. package/lib/tools/navigate.js +19 -23
  54. package/lib/tools/network.js +12 -14
  55. package/lib/tools/networkDetail.js +68 -61
  56. package/lib/tools/networkSearch/bodySearch.js +46 -32
  57. package/lib/tools/networkSearch/grouping.js +15 -6
  58. package/lib/tools/networkSearch/helpers.js +4 -4
  59. package/lib/tools/networkSearch/searchHtml.js +25 -16
  60. package/lib/tools/networkSearch/urlSearch.js +56 -14
  61. package/lib/tools/networkSearch.js +65 -35
  62. package/lib/tools/pdf.js +13 -12
  63. package/lib/tools/repl.js +66 -54
  64. package/lib/tools/screenshot.js +57 -33
  65. package/lib/tools/scroll.js +29 -24
  66. package/lib/tools/snapshot.js +66 -49
  67. package/lib/tools/tabs.js +22 -19
  68. package/lib/tools/tool.js +5 -3
  69. package/lib/tools/utils.js +17 -13
  70. package/lib/tools/wait.js +24 -19
  71. package/lib/tools.js +21 -20
  72. package/lib/utils/adBlockFilter.js +29 -26
  73. package/lib/utils/codegen.js +20 -16
  74. package/lib/utils/extensionPath.js +4 -4
  75. package/lib/utils/fileUtils.js +17 -13
  76. package/lib/utils/graphql.js +69 -58
  77. package/lib/utils/guid.js +3 -3
  78. package/lib/utils/httpServer.js +9 -9
  79. package/lib/utils/log.js +3 -3
  80. package/lib/utils/manualPromise.js +7 -7
  81. package/lib/utils/networkFormat.js +7 -5
  82. package/lib/utils/package.js +4 -4
  83. package/lib/utils/sanitizeHtml.js +66 -34
  84. package/lib/utils/truncate.js +25 -25
  85. package/lib/utils/withTimeout.js +1 -1
  86. package/package.json +34 -57
  87. package/src/index.ts +27 -17
  88. package/LICENSE +0 -202
@@ -13,21 +13,23 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
18
- import { elementSchema } from './snapshot.js';
19
- import { generateLocator } from './utils.js';
20
- import * as javascript from '../utils/codegen.js';
16
+ import { z } from "zod";
17
+ import * as javascript from "../utils/codegen.js";
18
+ import { elementSchema } from "./snapshot.js";
19
+ import { defineTabTool } from "./tool.js";
20
+ import { generateLocator } from "./utils.js";
21
21
  const pressKey = defineTabTool({
22
- capability: 'core',
22
+ capability: "core",
23
23
  schema: {
24
- name: 'browser_press_key',
25
- title: 'Press a key',
26
- description: 'Press a key on the keyboard',
24
+ name: "browser_press_key",
25
+ title: "Press a key",
26
+ description: "Press a key on the keyboard",
27
27
  inputSchema: z.object({
28
- key: z.string().describe('Name of the key to press or a character to generate, such as `ArrowLeft` or `a`'),
28
+ key: z
29
+ .string()
30
+ .describe("Name of the key to press or a character to generate, such as `ArrowLeft` or `a`"),
29
31
  }),
30
- type: 'input',
32
+ type: "input",
31
33
  },
32
34
  handle: async (tab, params, response) => {
33
35
  response.setIncludeSnapshot();
@@ -39,18 +41,24 @@ const pressKey = defineTabTool({
39
41
  },
40
42
  });
41
43
  const typeSchema = elementSchema.extend({
42
- text: z.string().describe('Text to type into the element'),
43
- submit: z.boolean().optional().describe('Whether to submit entered text (press Enter after)'),
44
- slowly: z.boolean().optional().describe('Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.'),
44
+ text: z.string().describe("Text to type into the element"),
45
+ submit: z
46
+ .boolean()
47
+ .optional()
48
+ .describe("Whether to submit entered text (press Enter after)"),
49
+ slowly: z
50
+ .boolean()
51
+ .optional()
52
+ .describe("Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once."),
45
53
  });
46
54
  const type = defineTabTool({
47
- capability: 'core',
55
+ capability: "core",
48
56
  schema: {
49
- name: 'browser_type',
50
- title: 'Type text',
51
- description: 'Type text into editable element',
57
+ name: "browser_type",
58
+ title: "Type text",
59
+ description: "Type text into editable element",
52
60
  inputSchema: typeSchema,
53
- type: 'input',
61
+ type: "input",
54
62
  },
55
63
  handle: async (tab, params, response) => {
56
64
  const locator = await tab.refLocator(params);
@@ -67,12 +75,9 @@ const type = defineTabTool({
67
75
  if (params.submit) {
68
76
  response.setIncludeSnapshot();
69
77
  response.addCode(`await page.${await generateLocator(locator)}.press('Enter');`);
70
- await locator.press('Enter');
78
+ await locator.press("Enter");
71
79
  }
72
80
  });
73
81
  },
74
82
  });
75
- export default [
76
- pressKey,
77
- type,
78
- ];
83
+ export default [pressKey, type];
@@ -13,22 +13,24 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
16
+ import { z } from "zod";
17
+ import { defineTabTool } from "./tool.js";
18
18
  const elementSchema = z.object({
19
- element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
19
+ element: z
20
+ .string()
21
+ .describe("Human-readable element description used to obtain permission to interact with the element"),
20
22
  });
21
23
  const mouseMove = defineTabTool({
22
- capability: 'vision',
24
+ capability: "vision",
23
25
  schema: {
24
- name: 'browser_mouse_move_xy',
25
- title: 'Move mouse',
26
- description: 'Move mouse to a given position',
26
+ name: "browser_mouse_move_xy",
27
+ title: "Move mouse",
28
+ description: "Move mouse to a given position",
27
29
  inputSchema: elementSchema.extend({
28
- x: z.number().describe('X coordinate'),
29
- y: z.number().describe('Y coordinate'),
30
+ x: z.number().describe("X coordinate"),
31
+ y: z.number().describe("Y coordinate"),
30
32
  }),
31
- type: 'readOnly',
33
+ type: "readOnly",
32
34
  },
33
35
  handle: async (tab, params, response) => {
34
36
  response.addCode(`// Move mouse to (${params.x}, ${params.y})`);
@@ -39,16 +41,16 @@ const mouseMove = defineTabTool({
39
41
  },
40
42
  });
41
43
  const mouseClick = defineTabTool({
42
- capability: 'vision',
44
+ capability: "vision",
43
45
  schema: {
44
- name: 'browser_mouse_click_xy',
45
- title: 'Click',
46
- description: 'Click left mouse button at a given position',
46
+ name: "browser_mouse_click_xy",
47
+ title: "Click",
48
+ description: "Click left mouse button at a given position",
47
49
  inputSchema: elementSchema.extend({
48
- x: z.number().describe('X coordinate'),
49
- y: z.number().describe('Y coordinate'),
50
+ x: z.number().describe("X coordinate"),
51
+ y: z.number().describe("Y coordinate"),
50
52
  }),
51
- type: 'input',
53
+ type: "input",
52
54
  },
53
55
  handle: async (tab, params, response) => {
54
56
  response.setIncludeSnapshot();
@@ -64,18 +66,18 @@ const mouseClick = defineTabTool({
64
66
  },
65
67
  });
66
68
  const mouseDrag = defineTabTool({
67
- capability: 'vision',
69
+ capability: "vision",
68
70
  schema: {
69
- name: 'browser_mouse_drag_xy',
70
- title: 'Drag mouse',
71
- description: 'Drag left mouse button to a given position',
71
+ name: "browser_mouse_drag_xy",
72
+ title: "Drag mouse",
73
+ description: "Drag left mouse button to a given position",
72
74
  inputSchema: elementSchema.extend({
73
- startX: z.number().describe('Start X coordinate'),
74
- startY: z.number().describe('Start Y coordinate'),
75
- endX: z.number().describe('End X coordinate'),
76
- endY: z.number().describe('End Y coordinate'),
75
+ startX: z.number().describe("Start X coordinate"),
76
+ startY: z.number().describe("Start Y coordinate"),
77
+ endX: z.number().describe("End X coordinate"),
78
+ endY: z.number().describe("End Y coordinate"),
77
79
  }),
78
- type: 'input',
80
+ type: "input",
79
81
  },
80
82
  handle: async (tab, params, response) => {
81
83
  response.setIncludeSnapshot();
@@ -92,8 +94,4 @@ const mouseDrag = defineTabTool({
92
94
  });
93
95
  },
94
96
  });
95
- export default [
96
- mouseMove,
97
- mouseClick,
98
- mouseDrag,
99
- ];
97
+ export default [mouseMove, mouseClick, mouseDrag];
@@ -13,18 +13,18 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTool, defineTabTool } from './tool.js';
16
+ import { z } from "zod";
17
+ import { defineTabTool, defineTool } from "./tool.js";
18
18
  const navigate = defineTool({
19
- capability: 'core',
19
+ capability: "core",
20
20
  schema: {
21
- name: 'browser_navigate',
22
- title: 'Navigate to a URL',
23
- description: 'Navigate to a URL',
21
+ name: "browser_navigate",
22
+ title: "Navigate to a URL",
23
+ description: "Navigate to a URL",
24
24
  inputSchema: z.object({
25
- url: z.string().describe('The URL to navigate to'),
25
+ url: z.string().describe("The URL to navigate to"),
26
26
  }),
27
- type: 'action',
27
+ type: "action",
28
28
  },
29
29
  handle: async (context, params, response) => {
30
30
  const tab = await context.ensureTab();
@@ -34,13 +34,13 @@ const navigate = defineTool({
34
34
  },
35
35
  });
36
36
  const goBack = defineTabTool({
37
- capability: 'core',
37
+ capability: "core",
38
38
  schema: {
39
- name: 'browser_navigate_back',
40
- title: 'Go back',
41
- description: 'Go back to the previous page',
39
+ name: "browser_navigate_back",
40
+ title: "Go back",
41
+ description: "Go back to the previous page",
42
42
  inputSchema: z.object({}),
43
- type: 'readOnly',
43
+ type: "readOnly",
44
44
  },
45
45
  handle: async (tab, params, response) => {
46
46
  await tab.page.goBack();
@@ -49,13 +49,13 @@ const goBack = defineTabTool({
49
49
  },
50
50
  });
51
51
  const goForward = defineTabTool({
52
- capability: 'core',
52
+ capability: "core",
53
53
  schema: {
54
- name: 'browser_navigate_forward',
55
- title: 'Go forward',
56
- description: 'Go forward to the next page',
54
+ name: "browser_navigate_forward",
55
+ title: "Go forward",
56
+ description: "Go forward to the next page",
57
57
  inputSchema: z.object({}),
58
- type: 'readOnly',
58
+ type: "readOnly",
59
59
  },
60
60
  handle: async (tab, params, response) => {
61
61
  await tab.page.goForward();
@@ -63,8 +63,4 @@ const goForward = defineTabTool({
63
63
  response.addCode(`await page.goForward();`);
64
64
  },
65
65
  });
66
- export default [
67
- navigate,
68
- goBack,
69
- goForward,
70
- ];
66
+ export default [navigate, goBack, goForward];
@@ -13,9 +13,9 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
18
- import { formatUrlWithTrimmedParams } from '../hooks/networkFilters.js';
16
+ import { z } from "zod";
17
+ import { formatUrlWithTrimmedParams } from "../hooks/networkFilters.js";
18
+ import { defineTabTool } from "./tool.js";
19
19
  // const requests = defineTool({
20
20
  // capability: 'core',
21
21
  // schema: {
@@ -61,13 +61,13 @@ import { formatUrlWithTrimmedParams } from '../hooks/networkFilters.js';
61
61
  // },
62
62
  // });
63
63
  const requests = defineTabTool({
64
- capability: 'core',
64
+ capability: "core",
65
65
  schema: {
66
- name: 'browser_network_requests',
67
- title: 'List network requests',
68
- description: 'Returns all network requests since loading the page',
66
+ name: "browser_network_requests",
67
+ title: "List network requests",
68
+ description: "Returns all network requests since loading the page",
69
69
  inputSchema: z.object({}),
70
- type: 'readOnly',
70
+ type: "readOnly",
71
71
  },
72
72
  handle: async (tab, params, response) => {
73
73
  const requests = tab.requests();
@@ -82,8 +82,8 @@ export async function renderRequest(request, response) {
82
82
  result.push(`[${request.method().toUpperCase()}] ${trimmedUrl}`);
83
83
  // Append response status if available with compact content-type
84
84
  if (response) {
85
- const ct = response.headers()?.['content-type'];
86
- result.push(`=> [${response.status()}] ${response.statusText()}${ct ? ` ct=${ct}` : ''}`);
85
+ const ct = response.headers()?.["content-type"];
86
+ result.push(`=> [${response.status()}] ${response.statusText()}${ct ? ` ct=${ct}` : ""}`);
87
87
  }
88
88
  // Always include request headers (excluding HTTP/2 pseudo-headers and common headers)
89
89
  // const allHeaders = await request.allHeaders();
@@ -116,8 +116,6 @@ export async function renderRequest(request, response) {
116
116
  // if (body)
117
117
  // result.push(`body=${body}`);
118
118
  // }
119
- return result.join('\n');
119
+ return result.join("\n");
120
120
  }
121
- export default [
122
- requests,
123
- ];
121
+ export default [requests];
@@ -1,26 +1,25 @@
1
- import { z } from 'zod';
2
- import ms from 'ms';
3
- import * as cheerio from 'cheerio';
4
- import { defineTabTool } from './tool.js';
5
- import { removeNonEssentialLinks, removeMetaTags, removeHtmlComments, stripSvgAttributes, minifyHtml } from '../utils/sanitizeHtml.js';
6
- import { formatTruncationLine } from '../utils/truncate.js';
7
- import { withTimeout } from '../utils/withTimeout.js';
8
- import { getNetworkEventEntry } from '../hooks/networkSetup.js';
9
- import { parseGraphQLRequestFromHttp, summarizeGraphQL, extractGraphQLResponseInfo, minifyGraphQLQuery, minifyGraphQLRequestBody } from '../utils/graphql.js';
10
- import { formatNetworkSummaryLine } from '../utils/networkFormat.js';
11
- const TIMEOUT_MS = ms('5s');
1
+ import ms from "ms";
2
+ import { z } from "zod";
3
+ import { getNetworkEventEntry } from "../hooks/networkSetup.js";
4
+ import { extractGraphQLResponseInfo, minifyGraphQLQuery, minifyGraphQLRequestBody, parseGraphQLRequestFromHttp, summarizeGraphQL, } from "../utils/graphql.js";
5
+ import { formatNetworkSummaryLine } from "../utils/networkFormat.js";
6
+ import { sanitizeHtml } from "../utils/sanitizeHtml.js";
7
+ import { formatTruncationLine } from "../utils/truncate.js";
8
+ import { withTimeout } from "../utils/withTimeout.js";
9
+ import { defineTabTool } from "./tool.js";
10
+ const TIMEOUT_MS = ms("5s");
12
11
  const CHUNK_BYTES = 8 * 1024; // 8KB per page
13
12
  const contextBodyMap = new WeakMap();
14
13
  // Skip HTTP/2 pseudo-headers and their non-colon variants in displayed request headers
15
14
  const SKIP_REQUEST_HEADER_KEYS = new Set([
16
- ':authority',
17
- ':method',
18
- ':path',
19
- ':scheme',
20
- 'authority',
21
- 'method',
22
- 'path',
23
- 'scheme',
15
+ ":authority",
16
+ ":method",
17
+ ":path",
18
+ ":scheme",
19
+ "authority",
20
+ "method",
21
+ "path",
22
+ "scheme",
24
23
  ]);
25
24
  const getStateMap = (ctx) => {
26
25
  let m = contextBodyMap.get(ctx);
@@ -31,7 +30,7 @@ const getStateMap = (ctx) => {
31
30
  return m;
32
31
  };
33
32
  const networkDetailSchema = z.object({
34
- id: z.number().describe('The network event id from events log'),
33
+ id: z.number().describe("The network event id from events log"),
35
34
  });
36
35
  const normalizeHeaderKeys = (headers) => {
37
36
  const out = {};
@@ -40,7 +39,7 @@ const normalizeHeaderKeys = (headers) => {
40
39
  return out;
41
40
  };
42
41
  const formatBytes = (n) => {
43
- const units = ['B', 'KB', 'MB', 'GB'];
42
+ const units = ["B", "KB", "MB", "GB"];
44
43
  let i = 0;
45
44
  let v = n;
46
45
  while (v >= 1024 && i < units.length - 1) {
@@ -68,16 +67,16 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
68
67
  if (!Number.isNaN(duration))
69
68
  lines.push(`Duration: ${duration}ms`);
70
69
  }
71
- lines.push(`Failure: ${request.failure()?.errorText || 'None'}`);
70
+ lines.push(`Failure: ${request.failure()?.errorText || "None"}`);
72
71
  if (!response) {
73
72
  lines.push(`\nRequest Headers:`);
74
73
  const headerKeysNoResp = Object.keys(reqHeaders)
75
- .filter(k => !SKIP_REQUEST_HEADER_KEYS.has(k))
74
+ .filter((k) => !SKIP_REQUEST_HEADER_KEYS.has(k))
76
75
  .sort();
77
76
  for (const key of headerKeysNoResp)
78
77
  lines.push(`${key}: ${reqHeaders[key]}`);
79
78
  lines.push(`\nNo response received yet`);
80
- return lines.join('\n');
79
+ return lines.join("\n");
81
80
  }
82
81
  // Determine if this request is GraphQL (used for both request and response sections)
83
82
  const gqlReq = parseGraphQLRequestFromHttp(request.method(), request.url(), reqHeaders, requestBody);
@@ -97,25 +96,27 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
97
96
  // Print full request headers on the first call (excluding authority/method/path/scheme)
98
97
  lines.push(`\nRequest Headers:`);
99
98
  const headerKeys = Object.keys(reqHeaders)
100
- .filter(k => !SKIP_REQUEST_HEADER_KEYS.has(k))
99
+ .filter((k) => !SKIP_REQUEST_HEADER_KEYS.has(k))
101
100
  .sort();
102
101
  for (const key of headerKeys)
103
102
  lines.push(`${key}: ${reqHeaders[key]}`);
104
- const reqCt = reqHeaders['content-type'];
103
+ const reqCt = reqHeaders["content-type"];
105
104
  // GraphQL request summary (if applicable) and compact body handling
106
105
  if (gqlReq) {
107
106
  lines.push(`\nGraphQL Request: ${summarizeGraphQL(gqlReq)}`);
108
- if (gqlReq.variables && typeof gqlReq.variables === 'object' && !Array.isArray(gqlReq.variables)) {
107
+ if (gqlReq.variables &&
108
+ typeof gqlReq.variables === "object" &&
109
+ !Array.isArray(gqlReq.variables)) {
109
110
  const varKeys = Object.keys(gqlReq.variables);
110
111
  if (varKeys.length)
111
- lines.push(`Variables: { ${varKeys.join(', ')} }`);
112
+ lines.push(`Variables: { ${varKeys.join(", ")} }`);
112
113
  }
113
114
  }
114
115
  if (requestBody) {
115
116
  if (gqlReq) {
116
117
  lines.push(`\nRequest Body (GraphQL):`);
117
118
  try {
118
- if (reqCt && reqCt.includes('application/graphql')) {
119
+ if (reqCt && reqCt.includes("application/graphql")) {
119
120
  lines.push(minifyGraphQLQuery(requestBody));
120
121
  }
121
122
  else {
@@ -139,15 +140,23 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
139
140
  }
140
141
  }
141
142
  const resHeaders = normalizeHeaderKeys(await response.allHeaders());
142
- const resCt = resHeaders['content-type'];
143
- const resCl = resHeaders['content-length'];
143
+ const resCt = resHeaders["content-type"];
144
+ const resCl = resHeaders["content-length"];
144
145
  const headerParts = [];
145
146
  if (resCt)
146
147
  headerParts.push(`content-type=${resCt}`);
147
148
  if (resCl)
148
149
  headerParts.push(`content-length=${resCl}`);
149
150
  if (headerParts.length)
150
- lines.push(`\nResponse Headers: { ${headerParts.join(', ')} }`);
151
+ lines.push(`\nResponse Headers: { ${headerParts.join(", ")} }`);
152
+ const setCookieHeaders = await response
153
+ .headerValues("set-cookie")
154
+ .catch(() => []);
155
+ if (setCookieHeaders.length) {
156
+ lines.push(`\nSet-Cookie Headers (${setCookieHeaders.length}):`);
157
+ for (const cookie of setCookieHeaders)
158
+ lines.push(cookie);
159
+ }
151
160
  }
152
161
  // Fetch the full body each time
153
162
  let buf;
@@ -157,63 +166,61 @@ const formatRequestDetail = async (ctx, { request, response, id, }) => {
157
166
  catch (e) {
158
167
  const msg = e instanceof Error ? e.message : String(e);
159
168
  lines.push(`\nFailed to read response body: ${msg}`);
160
- return lines.join('\n');
169
+ return lines.join("\n");
161
170
  }
162
171
  // On first page, summarize GraphQL response if applicable
163
172
  if (!state.headersShown && state.isGraphQLRequest) {
164
173
  try {
165
- const fullText = buf.toString('utf8');
174
+ const fullText = buf.toString("utf8");
166
175
  const gqlInfo = extractGraphQLResponseInfo(fullText);
167
176
  if (gqlInfo) {
168
- lines.push(`\nGraphQL Response: ${gqlInfo.hasErrors ? `${gqlInfo.errorCount} error(s)` : 'ok'}`);
177
+ lines.push(`\nGraphQL Response: ${gqlInfo.hasErrors ? `${gqlInfo.errorCount} error(s)` : "ok"}`);
169
178
  if (gqlInfo.topMessages.length)
170
- lines.push(`Errors: ${gqlInfo.topMessages.join(' | ')}`);
179
+ lines.push(`Errors: ${gqlInfo.topMessages.join(" | ")}`);
171
180
  if (gqlInfo.dataKeys.length)
172
- lines.push(`Data keys: ${gqlInfo.dataKeys.join(', ')}`);
181
+ lines.push(`Data keys: ${gqlInfo.dataKeys.join(", ")}`);
173
182
  }
174
183
  }
175
184
  catch { }
176
185
  }
177
- // Reset if finished in a prior call
178
- if (state.cursor >= buf.length) {
186
+ let bodyText = buf.toString("utf8");
187
+ const resHeadersForBody = normalizeHeaderKeys(await response.allHeaders());
188
+ const resCtForBody = resHeadersForBody["content-type"] || "";
189
+ const isHtml = resCtForBody.includes("html");
190
+ if (isHtml)
191
+ bodyText = sanitizeHtml(bodyText, {
192
+ shouldRemoveScripts: false,
193
+ shouldRemoveStyles: true,
194
+ });
195
+ const totalLength = bodyText.length;
196
+ if (state.cursor >= totalLength) {
179
197
  state.cursor = 0;
180
198
  state.headersShown = false; // allow headers again after a full pass
181
199
  }
182
- let bodyText = buf.toString('utf8');
183
- const resHeadersForBody = normalizeHeaderKeys(await response.allHeaders());
184
- const resCtForBody = resHeadersForBody['content-type'] || '';
185
- const isHtml = resCtForBody.includes('html');
186
- if (isHtml) {
187
- const $ = cheerio.load(bodyText, { xmlMode: false });
188
- removeNonEssentialLinks($);
189
- removeMetaTags($);
190
- removeHtmlComments($);
191
- stripSvgAttributes($);
192
- bodyText = minifyHtml($.root().html() || '');
193
- }
194
- const totalLength = bodyText.length;
195
200
  const start = state.cursor;
196
201
  const end = Math.min(start + CHUNK_BYTES, totalLength);
197
202
  const chunk = bodyText.slice(start, end);
198
203
  const nextCursor = end;
199
204
  const more = nextCursor < totalLength;
200
- const percent = totalLength ? Math.round((nextCursor / totalLength) * 100) : 100;
201
- lines.push(`\nBody Chunk (text): ${percent}%${more ? ' (more)' : ' (done)'}:`);
205
+ const percent = totalLength
206
+ ? Math.round((nextCursor / totalLength) * 100)
207
+ : 100;
208
+ lines.push(`\nBody Chunk (text): ${percent}%${more ? " (more)" : " (done)"}:`);
202
209
  lines.push(chunk);
203
210
  if (more)
204
- lines.push(formatTruncationLine(nextCursor, buf.length, { formatter: formatBytes }));
211
+ lines.push(formatTruncationLine(nextCursor, totalLength, { formatter: formatBytes }));
205
212
  state.cursor = nextCursor;
206
213
  state.headersShown = true;
207
- return lines.join('\n');
214
+ return lines.join("\n");
208
215
  };
209
216
  const networkDetail = defineTabTool({
210
- capability: 'core',
217
+ capability: "core",
211
218
  schema: {
212
- name: 'browser_network_detail',
213
- title: 'Get network request detail',
214
- description: 'Show detailed info for a specific network request by event id. Call repeatedly to page through the body.',
219
+ name: "browser_network_detail",
220
+ title: "Get network request detail",
221
+ description: "Show detailed info for a specific network request by event id. Call repeatedly to page through the body.",
215
222
  inputSchema: networkDetailSchema,
216
- type: 'readOnly',
223
+ type: "readOnly",
217
224
  },
218
225
  handle: async (tab, params, r) => {
219
226
  const entry = getNetworkEventEntry(tab.context, params.id);