@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.
- package/cli-wrapper.js +15 -14
- package/cli.js +1 -1
- package/config.d.ts +11 -6
- package/index.d.ts +7 -5
- package/index.js +1 -1
- package/lib/browserContextFactory.js +131 -58
- package/lib/browserServerBackend.js +14 -12
- package/lib/config.js +60 -46
- package/lib/context.js +41 -39
- package/lib/extension/cdpRelay.js +67 -61
- package/lib/extension/extensionContextFactory.js +10 -10
- package/lib/frameworkPatterns.js +21 -21
- package/lib/hooks/antiBotDetectionHook.js +59 -52
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +21 -21
- package/lib/hooks/events.js +3 -3
- package/lib/hooks/formatToolCallEvent.js +3 -7
- package/lib/hooks/frameworkStateHook.js +40 -40
- package/lib/hooks/grouping.js +3 -3
- package/lib/hooks/jsonLdDetectionHook.js +44 -37
- package/lib/hooks/networkFilters.js +17 -17
- package/lib/hooks/networkSetup.js +9 -7
- package/lib/hooks/networkTrackingHook.js +21 -21
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +15 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +38 -38
- package/lib/hooks/waitHook.js +7 -7
- package/lib/index.js +12 -10
- package/lib/mcp/inProcessTransport.js +3 -4
- package/lib/mcp/proxyBackend.js +43 -28
- package/lib/mcp/server.js +24 -19
- package/lib/mcp/tool.js +14 -8
- package/lib/mcp/transport.js +60 -53
- package/lib/playwrightTransformer.js +129 -106
- package/lib/program.js +54 -52
- package/lib/response.js +36 -30
- package/lib/sessionLog.js +19 -17
- package/lib/tab.js +41 -39
- package/lib/tools/common.js +19 -19
- package/lib/tools/console.js +11 -11
- package/lib/tools/dialogs.js +18 -15
- package/lib/tools/evaluate.js +26 -17
- package/lib/tools/extractFrameworkState.js +48 -37
- package/lib/tools/files.js +17 -14
- package/lib/tools/form.js +32 -23
- package/lib/tools/getSnapshot.js +14 -15
- package/lib/tools/getVisibleHtml.js +33 -17
- package/lib/tools/install.js +20 -20
- package/lib/tools/keyboard.js +29 -24
- package/lib/tools/mouse.js +29 -31
- package/lib/tools/navigate.js +19 -23
- package/lib/tools/network.js +12 -14
- package/lib/tools/networkDetail.js +58 -49
- package/lib/tools/networkSearch/bodySearch.js +46 -32
- package/lib/tools/networkSearch/grouping.js +15 -6
- package/lib/tools/networkSearch/helpers.js +4 -4
- package/lib/tools/networkSearch/searchHtml.js +25 -16
- package/lib/tools/networkSearch/urlSearch.js +56 -14
- package/lib/tools/networkSearch.js +46 -36
- package/lib/tools/pdf.js +13 -12
- package/lib/tools/repl.js +66 -54
- package/lib/tools/screenshot.js +57 -33
- package/lib/tools/scroll.js +29 -24
- package/lib/tools/snapshot.js +66 -49
- package/lib/tools/tabs.js +22 -19
- package/lib/tools/tool.js +5 -3
- package/lib/tools/utils.js +17 -13
- package/lib/tools/wait.js +24 -19
- package/lib/tools.js +21 -20
- package/lib/utils/adBlockFilter.js +29 -26
- package/lib/utils/codegen.js +20 -16
- package/lib/utils/extensionPath.js +4 -4
- package/lib/utils/fileUtils.js +17 -13
- package/lib/utils/graphql.js +69 -58
- package/lib/utils/guid.js +3 -3
- package/lib/utils/httpServer.js +9 -9
- package/lib/utils/log.js +3 -3
- package/lib/utils/manualPromise.js +7 -7
- package/lib/utils/networkFormat.js +7 -5
- package/lib/utils/package.js +4 -4
- package/lib/utils/sanitizeHtml.js +66 -34
- package/lib/utils/truncate.js +25 -25
- package/lib/utils/withTimeout.js +1 -1
- package/package.json +34 -57
- package/src/index.ts +27 -17
- package/LICENSE +0 -202
|
@@ -1,28 +1,30 @@
|
|
|
1
|
-
import { withTimeout } from
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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 !==
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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
|
|
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()[
|
|
63
|
+
const requestContentType = request.headers()["content-type"] || "";
|
|
60
64
|
let handled = false;
|
|
61
|
-
if (requestContentType.includes(
|
|
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,
|
|
70
|
+
searchInObject(parsed, keyword, "request.body", matches, "requestBody");
|
|
65
71
|
handled = true;
|
|
66
72
|
}
|
|
67
73
|
catch { }
|
|
68
74
|
}
|
|
69
|
-
if (!handled &&
|
|
70
|
-
|
|
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 &&
|
|
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(
|
|
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,
|
|
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:
|
|
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:
|
|
113
|
+
path: "request.body",
|
|
106
114
|
value: body,
|
|
107
115
|
context: highlightMatch(body, keyword, CONTEXT_LENGTH_BODY),
|
|
108
|
-
source:
|
|
116
|
+
source: "requestBody",
|
|
109
117
|
});
|
|
110
118
|
}
|
|
111
119
|
};
|
|
112
120
|
export const searchInResponseBody = async (response, keyword, matches) => {
|
|
113
|
-
const contentTypeHeader = response.headers()[
|
|
114
|
-
if (!(contentTypeHeader.startsWith(
|
|
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(
|
|
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,
|
|
140
|
+
searchInObject(parsed, keyword, "response.body", matches, "responseBody");
|
|
129
141
|
handled = true;
|
|
130
142
|
}
|
|
131
143
|
catch { }
|
|
132
144
|
}
|
|
133
|
-
if (!handled &&
|
|
134
|
-
|
|
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:
|
|
153
|
+
path: "response.body",
|
|
140
154
|
value: responseBody,
|
|
141
155
|
context: highlightMatch(responseBody, keyword, CONTEXT_LENGTH_BODY),
|
|
142
|
-
source:
|
|
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,
|
|
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, {
|
|
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 &&
|
|
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, {
|
|
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
|
|
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
|
|
2
|
-
import { highlightMatch } from
|
|
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 &&
|
|
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(
|
|
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(
|
|
16
|
+
const classes = current.attr("class");
|
|
15
17
|
if (classes)
|
|
16
|
-
selector += `.${classes.trim().split(/\s+/).join(
|
|
17
|
-
const tagName = current[0].type ===
|
|
18
|
-
const siblings = current
|
|
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
|
-
$(
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
$(
|
|
49
|
-
if (el.type ===
|
|
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 ===
|
|
60
|
+
if (typeof val === "string" && val.toLowerCase().includes(keyword)) {
|
|
54
61
|
const elPath = getElementPath($(el));
|
|
55
|
-
const path = elPath
|
|
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
|
|
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(
|
|
7
|
+
const keywordPathOnly = keyword.split("?")[0];
|
|
8
8
|
if (reqUrlLower.includes(keyword)) {
|
|
9
|
-
matches.push({
|
|
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({
|
|
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) ||
|
|
19
|
-
|
|
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 ||
|
|
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({
|
|
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({
|
|
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({
|
|
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) ||
|
|
56
|
-
|
|
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 ||
|
|
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({
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import { getNetworkEventEntry } from
|
|
4
|
-
import { listNetworkEvents } from
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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
|
|
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(
|
|
57
|
-
? withoutWildcard.slice(
|
|
58
|
-
: withoutWildcard.startsWith(
|
|
59
|
-
? withoutWildcard.slice(
|
|
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(
|
|
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:
|
|
93
|
+
source: "responseHeaders",
|
|
92
94
|
});
|
|
93
95
|
}
|
|
94
96
|
};
|
|
95
97
|
const networkSearch = defineTool({
|
|
96
|
-
capability:
|
|
98
|
+
capability: "core",
|
|
97
99
|
schema: {
|
|
98
|
-
name:
|
|
99
|
-
title:
|
|
100
|
-
description:
|
|
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:
|
|
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(
|
|
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 = {
|
|
140
|
-
|
|
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 ||
|
|
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 ||
|
|
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 ||
|
|
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
|
}
|