@wordbricks/playwright-mcp 0.1.20 → 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 (87) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/lib/browserContextFactory.js +131 -58
  7. package/lib/browserServerBackend.js +14 -12
  8. package/lib/config.js +60 -46
  9. package/lib/context.js +41 -39
  10. package/lib/extension/cdpRelay.js +67 -61
  11. package/lib/extension/extensionContextFactory.js +10 -10
  12. package/lib/frameworkPatterns.js +21 -21
  13. package/lib/hooks/antiBotDetectionHook.js +59 -52
  14. package/lib/hooks/core.js +11 -10
  15. package/lib/hooks/eventConsumer.js +21 -21
  16. package/lib/hooks/events.js +3 -3
  17. package/lib/hooks/formatToolCallEvent.js +3 -7
  18. package/lib/hooks/frameworkStateHook.js +40 -40
  19. package/lib/hooks/grouping.js +3 -3
  20. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  21. package/lib/hooks/networkFilters.js +17 -17
  22. package/lib/hooks/networkSetup.js +9 -7
  23. package/lib/hooks/networkTrackingHook.js +21 -21
  24. package/lib/hooks/pageHeightHook.js +9 -9
  25. package/lib/hooks/registry.js +15 -16
  26. package/lib/hooks/requireTabHook.js +3 -3
  27. package/lib/hooks/schema.js +38 -38
  28. package/lib/hooks/waitHook.js +7 -7
  29. package/lib/index.js +12 -10
  30. package/lib/mcp/inProcessTransport.js +3 -4
  31. package/lib/mcp/proxyBackend.js +43 -28
  32. package/lib/mcp/server.js +24 -19
  33. package/lib/mcp/tool.js +14 -8
  34. package/lib/mcp/transport.js +60 -53
  35. package/lib/playwrightTransformer.js +129 -106
  36. package/lib/program.js +54 -52
  37. package/lib/response.js +36 -30
  38. package/lib/sessionLog.js +19 -17
  39. package/lib/tab.js +41 -39
  40. package/lib/tools/common.js +19 -19
  41. package/lib/tools/console.js +11 -11
  42. package/lib/tools/dialogs.js +18 -15
  43. package/lib/tools/evaluate.js +26 -17
  44. package/lib/tools/extractFrameworkState.js +48 -37
  45. package/lib/tools/files.js +17 -14
  46. package/lib/tools/form.js +32 -23
  47. package/lib/tools/getSnapshot.js +14 -15
  48. package/lib/tools/getVisibleHtml.js +33 -17
  49. package/lib/tools/install.js +20 -20
  50. package/lib/tools/keyboard.js +29 -24
  51. package/lib/tools/mouse.js +29 -31
  52. package/lib/tools/navigate.js +19 -23
  53. package/lib/tools/network.js +12 -14
  54. package/lib/tools/networkDetail.js +58 -49
  55. package/lib/tools/networkSearch/bodySearch.js +46 -32
  56. package/lib/tools/networkSearch/grouping.js +15 -6
  57. package/lib/tools/networkSearch/helpers.js +4 -4
  58. package/lib/tools/networkSearch/searchHtml.js +25 -16
  59. package/lib/tools/networkSearch/urlSearch.js +56 -14
  60. package/lib/tools/networkSearch.js +46 -36
  61. package/lib/tools/pdf.js +13 -12
  62. package/lib/tools/repl.js +66 -54
  63. package/lib/tools/screenshot.js +57 -33
  64. package/lib/tools/scroll.js +29 -24
  65. package/lib/tools/snapshot.js +66 -49
  66. package/lib/tools/tabs.js +22 -19
  67. package/lib/tools/tool.js +5 -3
  68. package/lib/tools/utils.js +17 -13
  69. package/lib/tools/wait.js +24 -19
  70. package/lib/tools.js +21 -20
  71. package/lib/utils/adBlockFilter.js +29 -26
  72. package/lib/utils/codegen.js +20 -16
  73. package/lib/utils/extensionPath.js +4 -4
  74. package/lib/utils/fileUtils.js +17 -13
  75. package/lib/utils/graphql.js +69 -58
  76. package/lib/utils/guid.js +3 -3
  77. package/lib/utils/httpServer.js +9 -9
  78. package/lib/utils/log.js +3 -3
  79. package/lib/utils/manualPromise.js +7 -7
  80. package/lib/utils/networkFormat.js +7 -5
  81. package/lib/utils/package.js +4 -4
  82. package/lib/utils/sanitizeHtml.js +66 -34
  83. package/lib/utils/truncate.js +25 -25
  84. package/lib/utils/withTimeout.js +1 -1
  85. package/package.json +34 -57
  86. package/src/index.ts +27 -17
  87. package/LICENSE +0 -202
@@ -1,28 +1,30 @@
1
- import { withTimeout } from '../../utils/withTimeout.js';
2
- import { searchInHtml } from './searchHtml.js';
3
- import { highlightMatch } from './helpers.js';
1
+ import { withTimeout } from "../../utils/withTimeout.js";
2
+ import { highlightMatch } from "./helpers.js";
3
+ import { searchInHtml } from "./searchHtml.js";
4
4
  const CONTEXT_LENGTH_BODY = 400;
5
5
  const CONTEXT_LENGTH_DEFAULT = 300;
6
6
  const MAX_SEARCH_DEPTH = 20;
7
7
  export const searchInObject = (obj, keyword, path, matches, source, depth = 0) => {
8
- if (depth > MAX_SEARCH_DEPTH || !obj || typeof obj !== 'object')
8
+ if (depth > MAX_SEARCH_DEPTH || !obj || typeof obj !== "object")
9
9
  return;
10
10
  for (const key in obj) {
11
11
  const newPath = path ? `${path}.${key}` : key;
12
12
  const value = obj[key];
13
13
  if (key.toLowerCase().includes(keyword)) {
14
14
  const highlightedKey = highlightMatch(key, keyword, CONTEXT_LENGTH_DEFAULT);
15
- const valueContext = typeof value === 'object' && value !== null
15
+ const valueContext = typeof value === "object" && value !== null
16
16
  ? truncateJsonPreview(value)
17
17
  : truncateStringPreview(String(value));
18
18
  matches.push({
19
19
  path: newPath,
20
- value: typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value),
20
+ value: typeof value === "object" && value !== null
21
+ ? JSON.stringify(value)
22
+ : String(value),
21
23
  context: `${highlightedKey}: ${valueContext}`,
22
24
  source,
23
25
  });
24
26
  }
25
- if (typeof value === 'string' && value.toLowerCase().includes(keyword)) {
27
+ if (typeof value === "string" && value.toLowerCase().includes(keyword)) {
26
28
  matches.push({
27
29
  path: newPath,
28
30
  value: value,
@@ -30,7 +32,7 @@ export const searchInObject = (obj, keyword, path, matches, source, depth = 0) =
30
32
  source,
31
33
  });
32
34
  }
33
- else if (typeof value === 'number' && String(value).includes(keyword)) {
35
+ else if (typeof value === "number" && String(value).includes(keyword)) {
34
36
  const valueStr = String(value);
35
37
  matches.push({
36
38
  path: newPath,
@@ -39,50 +41,56 @@ export const searchInObject = (obj, keyword, path, matches, source, depth = 0) =
39
41
  source,
40
42
  });
41
43
  }
42
- if (typeof value === 'object' && value !== null)
44
+ if (typeof value === "object" && value !== null)
43
45
  searchInObject(value, keyword, newPath, matches, source, depth + 1);
44
46
  }
45
47
  };
46
- const truncateStringPreview = (text) => text.length > CONTEXT_LENGTH_DEFAULT ? text.slice(0, CONTEXT_LENGTH_DEFAULT) + '...' : text;
48
+ const truncateStringPreview = (text) => text.length > CONTEXT_LENGTH_DEFAULT
49
+ ? text.slice(0, CONTEXT_LENGTH_DEFAULT) + "..."
50
+ : text;
47
51
  const truncateJsonPreview = (obj) => {
48
52
  try {
49
- return JSON.stringify(obj).slice(0, CONTEXT_LENGTH_BODY) + '...';
53
+ return JSON.stringify(obj).slice(0, CONTEXT_LENGTH_BODY) + "...";
50
54
  }
51
55
  catch {
52
- return '[Object]';
56
+ return "[Object]";
53
57
  }
54
58
  };
55
59
  export const searchInRequestBody = (request, keyword, matches) => {
56
60
  const body = request.postData();
57
61
  if (!body || !body.toLowerCase().includes(keyword))
58
62
  return;
59
- const requestContentType = request.headers()['content-type'] || '';
63
+ const requestContentType = request.headers()["content-type"] || "";
60
64
  let handled = false;
61
- if (requestContentType.includes('application/json') || body.trim().startsWith('{') || body.trim().startsWith('[')) {
65
+ if (requestContentType.includes("application/json") ||
66
+ body.trim().startsWith("{") ||
67
+ body.trim().startsWith("[")) {
62
68
  try {
63
69
  const parsed = JSON.parse(body);
64
- searchInObject(parsed, keyword, 'request.body', matches, 'requestBody');
70
+ searchInObject(parsed, keyword, "request.body", matches, "requestBody");
65
71
  handled = true;
66
72
  }
67
73
  catch { }
68
74
  }
69
- if (!handled && (requestContentType.includes('text/html') || body.trim().startsWith('<'))) {
70
- searchInHtml(body, keyword, 'request.body', matches, 'requestBody');
75
+ if (!handled &&
76
+ (requestContentType.includes("text/html") || body.trim().startsWith("<"))) {
77
+ searchInHtml(body, keyword, "request.body", matches, "requestBody");
71
78
  handled = true;
72
79
  }
73
- if (!handled && requestContentType.includes('application/x-www-form-urlencoded')) {
80
+ if (!handled &&
81
+ requestContentType.includes("application/x-www-form-urlencoded")) {
74
82
  try {
75
83
  const sp = new URLSearchParams(body);
76
84
  for (const [k, v] of sp.entries()) {
77
- const vStr = v || '';
85
+ const vStr = v || "";
78
86
  if (!vStr.toLowerCase().includes(keyword))
79
87
  continue;
80
88
  const trimmed = vStr.trim();
81
89
  let parsedOk = false;
82
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
90
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
83
91
  try {
84
92
  const parsed = JSON.parse(trimmed);
85
- searchInObject(parsed, keyword, `request.body.form.${k}`, matches, 'requestBody');
93
+ searchInObject(parsed, keyword, `request.body.form.${k}`, matches, "requestBody");
86
94
  parsedOk = true;
87
95
  }
88
96
  catch { }
@@ -92,7 +100,7 @@ export const searchInRequestBody = (request, keyword, matches) => {
92
100
  path: `request.body.form.${k}`,
93
101
  value: vStr,
94
102
  context: `${k}=${highlightMatch(vStr, keyword, CONTEXT_LENGTH_DEFAULT)}`,
95
- source: 'requestBody',
103
+ source: "requestBody",
96
104
  });
97
105
  }
98
106
  }
@@ -102,16 +110,18 @@ export const searchInRequestBody = (request, keyword, matches) => {
102
110
  }
103
111
  if (!handled) {
104
112
  matches.push({
105
- path: 'request.body',
113
+ path: "request.body",
106
114
  value: body,
107
115
  context: highlightMatch(body, keyword, CONTEXT_LENGTH_BODY),
108
- source: 'requestBody',
116
+ source: "requestBody",
109
117
  });
110
118
  }
111
119
  };
112
120
  export const searchInResponseBody = async (response, keyword, matches) => {
113
- const contentTypeHeader = response.headers()['content-type'] || '';
114
- if (!(contentTypeHeader.startsWith('text/') || contentTypeHeader.includes('application/json') || contentTypeHeader.includes('application/xml')))
121
+ const contentTypeHeader = response.headers()["content-type"] || "";
122
+ if (!(contentTypeHeader.startsWith("text/") ||
123
+ contentTypeHeader.includes("application/json") ||
124
+ contentTypeHeader.includes("application/xml")))
115
125
  return;
116
126
  try {
117
127
  const responseBody = await withTimeout(response.text(), 2000);
@@ -122,24 +132,28 @@ export const searchInResponseBody = async (response, keyword, matches) => {
122
132
  const xssiPrefix = ")]}'";
123
133
  const hasXssi = trimmed.startsWith(xssiPrefix);
124
134
  const jsonCandidate = hasXssi ? trimmed.slice(xssiPrefix.length) : trimmed;
125
- if (contentTypeHeader.includes('application/json') || jsonCandidate.startsWith('{') || jsonCandidate.startsWith('[')) {
135
+ if (contentTypeHeader.includes("application/json") ||
136
+ jsonCandidate.startsWith("{") ||
137
+ jsonCandidate.startsWith("[")) {
126
138
  try {
127
139
  const parsed = JSON.parse(jsonCandidate);
128
- searchInObject(parsed, keyword, 'response.body', matches, 'responseBody');
140
+ searchInObject(parsed, keyword, "response.body", matches, "responseBody");
129
141
  handled = true;
130
142
  }
131
143
  catch { }
132
144
  }
133
- if (!handled && (contentTypeHeader.includes('text/html') || responseBody.trim().startsWith('<'))) {
134
- searchInHtml(responseBody, keyword, 'response.body', matches, 'responseBody');
145
+ if (!handled &&
146
+ (contentTypeHeader.includes("text/html") ||
147
+ responseBody.trim().startsWith("<"))) {
148
+ searchInHtml(responseBody, keyword, "response.body", matches, "responseBody");
135
149
  handled = true;
136
150
  }
137
151
  if (!handled) {
138
152
  matches.push({
139
- path: 'response.body',
153
+ path: "response.body",
140
154
  value: responseBody,
141
155
  context: highlightMatch(responseBody, keyword, CONTEXT_LENGTH_BODY),
142
- source: 'responseBody',
156
+ source: "responseBody",
143
157
  });
144
158
  }
145
159
  }
@@ -1,7 +1,7 @@
1
1
  export const normalizePath = (path) => {
2
- if (path.includes(' > '))
3
- return path.replace(/:nth-child\(\d+\)/g, ':nth-child(*)');
4
- return path.replace(/\.(?:\d+)(?=\.|$)/g, '.*');
2
+ if (path.includes(" > "))
3
+ return path.replace(/:nth-child\(\d+\)/g, ":nth-child(*)");
4
+ return path.replace(/\.(?:\d+)(?=\.|$)/g, ".*");
5
5
  };
6
6
  export const getDepth = (path) => {
7
7
  const separators = path.match(/\.|>/g) || [];
@@ -10,18 +10,27 @@ export const getDepth = (path) => {
10
10
  export const mergeGroupedMatches = (a, b) => {
11
11
  const map = new Map();
12
12
  for (const g of a)
13
- map.set(g.normalized, { normalized: g.normalized, count: g.count, examples: g.examples.slice(0, 3) });
13
+ map.set(g.normalized, {
14
+ normalized: g.normalized,
15
+ count: g.count,
16
+ examples: g.examples.slice(0, 3),
17
+ });
14
18
  for (const g of b) {
15
19
  const existing = map.get(g.normalized);
16
20
  if (existing) {
17
21
  existing.count += g.count;
18
22
  for (const e of g.examples) {
19
- if (existing.examples.length < 3 && !existing.examples.some(x => x.context === e.context))
23
+ if (existing.examples.length < 3 &&
24
+ !existing.examples.some((x) => x.context === e.context))
20
25
  existing.examples.push(e);
21
26
  }
22
27
  }
23
28
  else {
24
- map.set(g.normalized, { normalized: g.normalized, count: g.count, examples: g.examples.slice(0, 3) });
29
+ map.set(g.normalized, {
30
+ normalized: g.normalized,
31
+ count: g.count,
32
+ examples: g.examples.slice(0, 3),
33
+ });
25
34
  }
26
35
  }
27
36
  return Array.from(map.values());
@@ -1,4 +1,4 @@
1
- import { truncateStringTo } from '../../utils/truncate.js';
1
+ import { truncateStringTo } from "../../utils/truncate.js";
2
2
  export const parseKeywordParams = (keyword) => {
3
3
  const out = [];
4
4
  try {
@@ -20,13 +20,13 @@ export const highlightMatch = (text, keyword, maxLength = 50) => {
20
20
  const contextAfter = maxLength - keyword.length - contextBefore;
21
21
  const start = Math.max(0, index - contextBefore);
22
22
  const end = Math.min(text.length, index + keyword.length + contextAfter);
23
- let result = '';
23
+ let result = "";
24
24
  if (start > 0)
25
- result += '...';
25
+ result += "...";
26
26
  result += text.substring(start, index);
27
27
  result += text.substring(index, index + keyword.length);
28
28
  result += text.substring(index + keyword.length, end);
29
29
  if (end < text.length)
30
- result += '...';
30
+ result += "...";
31
31
  return result;
32
32
  };
@@ -1,21 +1,26 @@
1
- import * as cheerio from 'cheerio';
2
- import { highlightMatch } from './helpers.js';
1
+ import * as cheerio from "cheerio";
2
+ import { highlightMatch } from "./helpers.js";
3
3
  export const getElementPath = ($el) => {
4
4
  const path = [];
5
5
  let current = $el;
6
- while (current.length && current[0].type === 'tag' && current[0].name !== 'html') {
6
+ while (current.length &&
7
+ current[0].type === "tag" &&
8
+ current[0].name !== "html") {
7
9
  let selector = current[0].name.toLowerCase();
8
- const id = current.attr('id');
10
+ const id = current.attr("id");
9
11
  if (id) {
10
12
  selector += `#${id}`;
11
13
  path.unshift(selector);
12
14
  break;
13
15
  }
14
- const classes = current.attr('class');
16
+ const classes = current.attr("class");
15
17
  if (classes)
16
- selector += `.${classes.trim().split(/\s+/).join('.')}`;
17
- const tagName = current[0].type === 'tag' ? current[0].name : '';
18
- const siblings = current.parent().children().filter((_, el) => el.type === 'tag' && el.name === tagName);
18
+ selector += `.${classes.trim().split(/\s+/).join(".")}`;
19
+ const tagName = current[0].type === "tag" ? current[0].name : "";
20
+ const siblings = current
21
+ .parent()
22
+ .children()
23
+ .filter((_, el) => el.type === "tag" && el.name === tagName);
19
24
  if (siblings.length > 1) {
20
25
  const index = siblings.index(current) + 1;
21
26
  selector += `:nth-child(${index})`;
@@ -23,13 +28,15 @@ export const getElementPath = ($el) => {
23
28
  path.unshift(selector);
24
29
  current = current.parent();
25
30
  }
26
- return path.join(' > ');
31
+ return path.join(" > ");
27
32
  };
28
33
  export const searchInHtml = (html, keyword, basePath, matches, source) => {
29
34
  const $ = cheerio.load(html);
30
- $('*').contents().each((_, node) => {
31
- if (node.type === 'text') {
32
- const text = (node.data || '').trim();
35
+ $("*")
36
+ .contents()
37
+ .each((_, node) => {
38
+ if (node.type === "text") {
39
+ const text = (node.data || "").trim();
33
40
  if (text && text.toLowerCase().includes(keyword)) {
34
41
  const parent = $(node).parent();
35
42
  if (parent.length) {
@@ -45,14 +52,16 @@ export const searchInHtml = (html, keyword, basePath, matches, source) => {
45
52
  }
46
53
  }
47
54
  });
48
- $('*').each((_, el) => {
49
- if (el.type === 'tag') {
55
+ $("*").each((_, el) => {
56
+ if (el.type === "tag") {
50
57
  const attrs = el.attribs;
51
58
  if (attrs) {
52
59
  Object.entries(attrs).forEach(([key, val]) => {
53
- if (typeof val === 'string' && val.toLowerCase().includes(keyword)) {
60
+ if (typeof val === "string" && val.toLowerCase().includes(keyword)) {
54
61
  const elPath = getElementPath($(el));
55
- const path = elPath ? `${basePath} > ${elPath}@${key}` : `${basePath}@${key}`;
62
+ const path = elPath
63
+ ? `${basePath} > ${elPath}@${key}`
64
+ : `${basePath}@${key}`;
56
65
  matches.push({
57
66
  path,
58
67
  value: val,
@@ -1,22 +1,38 @@
1
- import { highlightMatch } from './helpers.js';
1
+ import { highlightMatch } from "./helpers.js";
2
2
  const CONTEXT_LENGTH_DEFAULT = 300;
3
3
  export const searchInUrls = (request, response, keyword, keywordParams, matches) => {
4
4
  try {
5
5
  const reqUrl = request.url();
6
6
  const reqUrlLower = reqUrl.toLowerCase();
7
- const keywordPathOnly = keyword.split('?')[0];
7
+ const keywordPathOnly = keyword.split("?")[0];
8
8
  if (reqUrlLower.includes(keyword)) {
9
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
9
+ matches.push({
10
+ path: "request.url",
11
+ value: reqUrl,
12
+ context: `url: ${highlightMatch(reqUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`,
13
+ source: "requestUrl",
14
+ });
10
15
  }
11
16
  else if (keywordPathOnly && reqUrlLower.includes(keywordPathOnly)) {
12
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
17
+ matches.push({
18
+ path: "request.url",
19
+ value: reqUrl,
20
+ context: `url: ${highlightMatch(reqUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`,
21
+ source: "requestUrl",
22
+ });
13
23
  }
14
24
  else {
15
25
  try {
16
26
  const u = new URL(reqUrl);
17
27
  const pathnameLower = u.pathname.toLowerCase();
18
- if (pathnameLower.includes(keyword) || (keywordPathOnly && pathnameLower.includes(keywordPathOnly)))
19
- matches.push({ path: 'request.url', value: reqUrl, context: `url: ${highlightMatch(reqUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
28
+ if (pathnameLower.includes(keyword) ||
29
+ (keywordPathOnly && pathnameLower.includes(keywordPathOnly)))
30
+ matches.push({
31
+ path: "request.url",
32
+ value: reqUrl,
33
+ context: `url: ${highlightMatch(reqUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`,
34
+ source: "requestUrl",
35
+ });
20
36
  }
21
37
  catch { }
22
38
  }
@@ -29,12 +45,17 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
29
45
  const pageVal = u.searchParams.get(name);
30
46
  if (!pageVal)
31
47
  continue;
32
- const valLower = (value || '').toLowerCase();
48
+ const valLower = (value || "").toLowerCase();
33
49
  if (!valLower)
34
50
  continue;
35
51
  const pageValLower = pageVal.toLowerCase();
36
52
  if (pageValLower.includes(valLower))
37
- matches.push({ path: `request.url.query.${name}`, value: pageVal, context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`, source: 'requestUrl' });
53
+ matches.push({
54
+ path: `request.url.query.${name}`,
55
+ value: pageVal,
56
+ context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`,
57
+ source: "requestUrl",
58
+ });
38
59
  }
39
60
  }
40
61
  catch { }
@@ -43,17 +64,33 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
43
64
  const resUrl = response.url();
44
65
  const resUrlLower = resUrl.toLowerCase();
45
66
  if (resUrlLower.includes(keyword)) {
46
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
67
+ matches.push({
68
+ path: "response.url",
69
+ value: resUrl,
70
+ context: `url: ${highlightMatch(resUrl, keyword, CONTEXT_LENGTH_DEFAULT)}`,
71
+ source: "responseUrl",
72
+ });
47
73
  }
48
74
  else if (keywordPathOnly && resUrlLower.includes(keywordPathOnly)) {
49
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
75
+ matches.push({
76
+ path: "response.url",
77
+ value: resUrl,
78
+ context: `url: ${highlightMatch(resUrl, keywordPathOnly, CONTEXT_LENGTH_DEFAULT)}`,
79
+ source: "responseUrl",
80
+ });
50
81
  }
51
82
  else {
52
83
  try {
53
84
  const u2 = new URL(resUrl);
54
85
  const pathnameLower2 = u2.pathname.toLowerCase();
55
- if (pathnameLower2.includes(keyword) || (keywordPathOnly && pathnameLower2.includes(keywordPathOnly)))
56
- matches.push({ path: 'response.url', value: resUrl, context: `url: ${highlightMatch(resUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
86
+ if (pathnameLower2.includes(keyword) ||
87
+ (keywordPathOnly && pathnameLower2.includes(keywordPathOnly)))
88
+ matches.push({
89
+ path: "response.url",
90
+ value: resUrl,
91
+ context: `url: ${highlightMatch(resUrl, keywordPathOnly || keyword, CONTEXT_LENGTH_DEFAULT)}`,
92
+ source: "responseUrl",
93
+ });
57
94
  }
58
95
  catch { }
59
96
  }
@@ -66,12 +103,17 @@ export const searchInUrls = (request, response, keyword, keywordParams, matches)
66
103
  const pageVal = u2.searchParams.get(name);
67
104
  if (!pageVal)
68
105
  continue;
69
- const valLower = (value || '').toLowerCase();
106
+ const valLower = (value || "").toLowerCase();
70
107
  if (!valLower)
71
108
  continue;
72
109
  const pageValLower = pageVal.toLowerCase();
73
110
  if (pageValLower.includes(valLower))
74
- matches.push({ path: `response.url.query.${name}`, value: pageVal, context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`, source: 'responseUrl' });
111
+ matches.push({
112
+ path: `response.url.query.${name}`,
113
+ value: pageVal,
114
+ context: `${name}=${highlightMatch(pageVal, value, CONTEXT_LENGTH_DEFAULT)}`,
115
+ source: "responseUrl",
116
+ });
75
117
  }
76
118
  }
77
119
  catch { }
@@ -1,18 +1,20 @@
1
- import { z } from 'zod';
2
- import { defineTool } from './tool.js';
3
- import { getNetworkEventEntry } from '../hooks/networkSetup.js';
4
- import { listNetworkEvents } from '../hooks/networkTrackingHook.js';
5
- import { normalizeUrlForGrouping } from '../hooks/networkFilters.js';
6
- import { formatNetworkSummaryLine } from '../utils/networkFormat.js';
7
- import { toJsonPathNormalized, truncateStringTo } from '../utils/truncate.js';
8
- import { searchInUrls } from './networkSearch/urlSearch.js';
9
- import { searchInRequestBody, searchInResponseBody } from './networkSearch/bodySearch.js';
10
- import { normalizePath, getDepth, mergeGroupedMatches } from './networkSearch/grouping.js';
11
- import { parseKeywordParams, highlightMatch } from './networkSearch/helpers.js';
1
+ import { z } from "zod";
2
+ import { normalizeUrlForGrouping } from "../hooks/networkFilters.js";
3
+ import { getNetworkEventEntry } from "../hooks/networkSetup.js";
4
+ import { listNetworkEvents } from "../hooks/networkTrackingHook.js";
5
+ import { formatNetworkSummaryLine } from "../utils/networkFormat.js";
6
+ import { toJsonPathNormalized, truncateStringTo } from "../utils/truncate.js";
7
+ import { searchInRequestBody, searchInResponseBody, } from "./networkSearch/bodySearch.js";
8
+ import { getDepth, mergeGroupedMatches, normalizePath, } from "./networkSearch/grouping.js";
9
+ import { highlightMatch, parseKeywordParams } from "./networkSearch/helpers.js";
10
+ import { searchInUrls } from "./networkSearch/urlSearch.js";
11
+ import { defineTool } from "./tool.js";
12
12
  const MAX_SEARCH_RESULTS = 10;
13
13
  const MAX_GROUPS_TO_SHOW = 3;
14
14
  const networkSearchSchema = z.object({
15
- keyword: z.string().describe('Keyword or phrase; avoid generic words—specific terms reduce noise and improve precision.'),
15
+ keyword: z
16
+ .string()
17
+ .describe("Keyword or phrase; avoid generic words—specific terms reduce noise and improve precision."),
16
18
  });
17
19
  const createSourceCounts = () => ({
18
20
  requestUrl: 0,
@@ -43,22 +45,22 @@ const computeEventScore = (match) => {
43
45
  const requestBonus = match.sourceCounts.requestBody;
44
46
  const urlBonus = (match.sourceCounts.requestUrl + match.sourceCounts.responseUrl) * 0.25;
45
47
  const headerBonus = match.sourceCounts.responseHeaders;
46
- return base + responseBonus + requestBonus + urlBonus + headerBonus + recencyBoost;
48
+ return (base + responseBonus + requestBonus + urlBonus + headerBonus + recencyBoost);
47
49
  };
48
50
  const formatGroupPath = (group) => {
49
51
  const normalized = group.normalized;
50
52
  const jsonPath = toJsonPathNormalized(normalized);
51
53
  if (jsonPath)
52
54
  return `json: ${jsonPath.jsonPath}`;
53
- if (!normalized.includes(' > '))
55
+ if (!normalized.includes(" > "))
54
56
  return normalized;
55
- const withoutWildcard = normalized.replace(/:nth-child\(\*\)/g, '');
56
- const trimmedPrefix = withoutWildcard.startsWith('response.body')
57
- ? withoutWildcard.slice('response.body'.length).trim()
58
- : withoutWildcard.startsWith('request.body')
59
- ? withoutWildcard.slice('request.body'.length).trim()
57
+ const withoutWildcard = normalized.replace(/:nth-child\(\*\)/g, "");
58
+ const trimmedPrefix = withoutWildcard.startsWith("response.body")
59
+ ? withoutWildcard.slice("response.body".length).trim()
60
+ : withoutWildcard.startsWith("request.body")
61
+ ? withoutWildcard.slice("request.body".length).trim()
60
62
  : withoutWildcard;
61
- const noLeadingArrow = trimmedPrefix.startsWith('>')
63
+ const noLeadingArrow = trimmedPrefix.startsWith(">")
62
64
  ? trimmedPrefix.slice(1).trim()
63
65
  : trimmedPrefix;
64
66
  return `css: ${noLeadingArrow}`;
@@ -78,7 +80,7 @@ const extractExamples = (group) => {
78
80
  return items;
79
81
  };
80
82
  const searchInResponseSetCookies = async (response, keyword, matches) => {
81
- const cookies = await response.headerValues('set-cookie').catch(() => []);
83
+ const cookies = await response.headerValues("set-cookie").catch(() => []);
82
84
  if (!cookies.length)
83
85
  return;
84
86
  for (const [index, cookie] of cookies.entries()) {
@@ -88,25 +90,25 @@ const searchInResponseSetCookies = async (response, keyword, matches) => {
88
90
  path: `response.headers.set-cookie[${index}]`,
89
91
  value: cookie,
90
92
  context: highlightMatch(cookie, keyword, 120),
91
- source: 'responseHeaders',
93
+ source: "responseHeaders",
92
94
  });
93
95
  }
94
96
  };
95
97
  const networkSearch = defineTool({
96
- capability: 'core',
98
+ capability: "core",
97
99
  schema: {
98
- name: 'browser_network_search',
99
- title: 'Search network requests',
100
- description: 'Search for keywords in network request/response bodies and URLs',
100
+ name: "browser_network_search",
101
+ title: "Search network requests",
102
+ description: "Search for keywords in network request/response bodies and URLs",
101
103
  inputSchema: networkSearchSchema,
102
- type: 'readOnly',
104
+ type: "readOnly",
103
105
  },
104
106
  handle: async (context, params, response) => {
105
107
  await context.ensureTab();
106
108
  try {
107
109
  const keyword = params.keyword?.toLowerCase();
108
110
  if (!keyword) {
109
- response.addResult('keyword parameter is required');
111
+ response.addResult("keyword parameter is required");
110
112
  return;
111
113
  }
112
114
  const events = listNetworkEvents(context);
@@ -136,12 +138,17 @@ const networkSearch = defineTool({
136
138
  }
137
139
  group.count++;
138
140
  if (group.examples.length < 3) {
139
- const example = { context: m.context, value: m.value, originalPath: m.path !== normalized ? m.path : undefined };
140
- if (!group.examples.some(e => e.context === example.context))
141
+ const example = {
142
+ context: m.context,
143
+ value: m.value,
144
+ originalPath: m.path !== normalized ? m.path : undefined,
145
+ };
146
+ if (!group.examples.some((e) => e.context === example.context))
141
147
  group.examples.push(example);
142
148
  }
143
149
  }
144
- const groups = Array.from(groupMap.values()).sort((a, b) => b.count - a.count || getDepth(b.normalized) - getDepth(a.normalized));
150
+ const groups = Array.from(groupMap.values()).sort((a, b) => b.count - a.count ||
151
+ getDepth(b.normalized) - getDepth(a.normalized));
145
152
  const totalMatches = matches.length;
146
153
  const sourceCounts = accumulateSourceCounts(matches);
147
154
  searchMatches.push({
@@ -189,7 +196,9 @@ const networkSearch = defineTool({
189
196
  existing.groups = mergeGroupedMatches(existing.groups, m.groups);
190
197
  if (!existing.response && m.response)
191
198
  existing.response = m.response;
192
- const replacePrimary = eventScore > existing.primaryScore || (eventScore === existing.primaryScore && m.timestamp > existing.primaryTimestamp);
199
+ const replacePrimary = eventScore > existing.primaryScore ||
200
+ (eventScore === existing.primaryScore &&
201
+ m.timestamp > existing.primaryTimestamp);
193
202
  if (!replacePrimary)
194
203
  continue;
195
204
  existing.request = m.request;
@@ -211,7 +220,7 @@ const networkSearch = defineTool({
211
220
  const topMatches = aggregatedMatches.slice(0, MAX_SEARCH_RESULTS);
212
221
  const structured = {
213
222
  keyword: params.keyword,
214
- requests: topMatches.map(m => {
223
+ requests: topMatches.map((m) => {
215
224
  const method = m.request.method().toUpperCase();
216
225
  const url = m.request.url();
217
226
  // Normalize URL for grouping only; not returned in output
@@ -232,9 +241,10 @@ const networkSearch = defineTool({
232
241
  })();
233
242
  const primaryGroups = m.primaryGroups
234
243
  .slice()
235
- .sort((a, b) => b.count - a.count || getDepth(b.normalized) - getDepth(a.normalized))
244
+ .sort((a, b) => b.count - a.count ||
245
+ getDepth(b.normalized) - getDepth(a.normalized))
236
246
  .slice(0, MAX_GROUPS_TO_SHOW);
237
- const paths = primaryGroups.map(group => ({
247
+ const paths = primaryGroups.map((group) => ({
238
248
  path: formatGroupPath(group),
239
249
  examples: extractExamples(group),
240
250
  }));
@@ -256,7 +266,7 @@ const networkSearch = defineTool({
256
266
  params: queryKeys.slice(0, 6),
257
267
  paths,
258
268
  };
259
- })
269
+ }),
260
270
  };
261
271
  response.addResult(JSON.stringify(structured));
262
272
  }