@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.
- package/README.md +54 -44
- 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 +178 -0
- package/lib/hooks/core.js +11 -10
- package/lib/hooks/eventConsumer.js +29 -16
- 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 +24 -15
- package/lib/hooks/networkSetup.js +11 -6
- package/lib/hooks/networkTrackingHook.js +31 -19
- package/lib/hooks/pageHeightHook.js +9 -9
- package/lib/hooks/registry.js +18 -16
- package/lib/hooks/requireTabHook.js +3 -3
- package/lib/hooks/schema.js +44 -32
- 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 +68 -61
- 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 +65 -35
- 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,24 +1,27 @@
|
|
|
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,
|
|
19
21
|
requestBody: 0,
|
|
20
22
|
responseUrl: 0,
|
|
21
23
|
responseBody: 0,
|
|
24
|
+
responseHeaders: 0,
|
|
22
25
|
});
|
|
23
26
|
const accumulateSourceCounts = (records) => {
|
|
24
27
|
const counts = createSourceCounts();
|
|
@@ -31,6 +34,7 @@ const cloneSourceCounts = (counts) => ({
|
|
|
31
34
|
requestBody: counts.requestBody,
|
|
32
35
|
responseUrl: counts.responseUrl,
|
|
33
36
|
responseBody: counts.responseBody,
|
|
37
|
+
responseHeaders: counts.responseHeaders,
|
|
34
38
|
});
|
|
35
39
|
const computeEventScore = (match) => {
|
|
36
40
|
const ageMs = Math.max(0, Date.now() - match.timestamp);
|
|
@@ -40,22 +44,23 @@ const computeEventScore = (match) => {
|
|
|
40
44
|
const responseBonus = match.sourceCounts.responseBody * 2;
|
|
41
45
|
const requestBonus = match.sourceCounts.requestBody;
|
|
42
46
|
const urlBonus = (match.sourceCounts.requestUrl + match.sourceCounts.responseUrl) * 0.25;
|
|
43
|
-
|
|
47
|
+
const headerBonus = match.sourceCounts.responseHeaders;
|
|
48
|
+
return (base + responseBonus + requestBonus + urlBonus + headerBonus + recencyBoost);
|
|
44
49
|
};
|
|
45
50
|
const formatGroupPath = (group) => {
|
|
46
51
|
const normalized = group.normalized;
|
|
47
52
|
const jsonPath = toJsonPathNormalized(normalized);
|
|
48
53
|
if (jsonPath)
|
|
49
54
|
return `json: ${jsonPath.jsonPath}`;
|
|
50
|
-
if (!normalized.includes(
|
|
55
|
+
if (!normalized.includes(" > "))
|
|
51
56
|
return normalized;
|
|
52
|
-
const withoutWildcard = normalized.replace(/:nth-child\(\*\)/g,
|
|
53
|
-
const trimmedPrefix = withoutWildcard.startsWith(
|
|
54
|
-
? withoutWildcard.slice(
|
|
55
|
-
: withoutWildcard.startsWith(
|
|
56
|
-
? 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()
|
|
57
62
|
: withoutWildcard;
|
|
58
|
-
const noLeadingArrow = trimmedPrefix.startsWith(
|
|
63
|
+
const noLeadingArrow = trimmedPrefix.startsWith(">")
|
|
59
64
|
? trimmedPrefix.slice(1).trim()
|
|
60
65
|
: trimmedPrefix;
|
|
61
66
|
return `css: ${noLeadingArrow}`;
|
|
@@ -74,21 +79,36 @@ const extractExamples = (group) => {
|
|
|
74
79
|
}
|
|
75
80
|
return items;
|
|
76
81
|
};
|
|
82
|
+
const searchInResponseSetCookies = async (response, keyword, matches) => {
|
|
83
|
+
const cookies = await response.headerValues("set-cookie").catch(() => []);
|
|
84
|
+
if (!cookies.length)
|
|
85
|
+
return;
|
|
86
|
+
for (const [index, cookie] of cookies.entries()) {
|
|
87
|
+
if (!cookie.toLowerCase().includes(keyword))
|
|
88
|
+
continue;
|
|
89
|
+
matches.push({
|
|
90
|
+
path: `response.headers.set-cookie[${index}]`,
|
|
91
|
+
value: cookie,
|
|
92
|
+
context: highlightMatch(cookie, keyword, 120),
|
|
93
|
+
source: "responseHeaders",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
77
97
|
const networkSearch = defineTool({
|
|
78
|
-
capability:
|
|
98
|
+
capability: "core",
|
|
79
99
|
schema: {
|
|
80
|
-
name:
|
|
81
|
-
title:
|
|
82
|
-
description:
|
|
100
|
+
name: "browser_network_search",
|
|
101
|
+
title: "Search network requests",
|
|
102
|
+
description: "Search for keywords in network request/response bodies and URLs",
|
|
83
103
|
inputSchema: networkSearchSchema,
|
|
84
|
-
type:
|
|
104
|
+
type: "readOnly",
|
|
85
105
|
},
|
|
86
106
|
handle: async (context, params, response) => {
|
|
87
107
|
await context.ensureTab();
|
|
88
108
|
try {
|
|
89
109
|
const keyword = params.keyword?.toLowerCase();
|
|
90
110
|
if (!keyword) {
|
|
91
|
-
response.addResult(
|
|
111
|
+
response.addResult("keyword parameter is required");
|
|
92
112
|
return;
|
|
93
113
|
}
|
|
94
114
|
const events = listNetworkEvents(context);
|
|
@@ -103,8 +123,10 @@ const networkSearch = defineTool({
|
|
|
103
123
|
const matches = [];
|
|
104
124
|
searchInUrls(request, resp, keyword, keywordParams, matches);
|
|
105
125
|
searchInRequestBody(request, keyword, matches);
|
|
106
|
-
if (resp)
|
|
126
|
+
if (resp) {
|
|
107
127
|
await searchInResponseBody(resp, keyword, matches);
|
|
128
|
+
await searchInResponseSetCookies(resp, keyword, matches);
|
|
129
|
+
}
|
|
108
130
|
if (matches.length > 0) {
|
|
109
131
|
const groupMap = new Map();
|
|
110
132
|
for (const m of matches) {
|
|
@@ -116,12 +138,17 @@ const networkSearch = defineTool({
|
|
|
116
138
|
}
|
|
117
139
|
group.count++;
|
|
118
140
|
if (group.examples.length < 3) {
|
|
119
|
-
const example = {
|
|
120
|
-
|
|
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))
|
|
121
147
|
group.examples.push(example);
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
|
-
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));
|
|
125
152
|
const totalMatches = matches.length;
|
|
126
153
|
const sourceCounts = accumulateSourceCounts(matches);
|
|
127
154
|
searchMatches.push({
|
|
@@ -169,7 +196,9 @@ const networkSearch = defineTool({
|
|
|
169
196
|
existing.groups = mergeGroupedMatches(existing.groups, m.groups);
|
|
170
197
|
if (!existing.response && m.response)
|
|
171
198
|
existing.response = m.response;
|
|
172
|
-
const replacePrimary = eventScore > existing.primaryScore ||
|
|
199
|
+
const replacePrimary = eventScore > existing.primaryScore ||
|
|
200
|
+
(eventScore === existing.primaryScore &&
|
|
201
|
+
m.timestamp > existing.primaryTimestamp);
|
|
173
202
|
if (!replacePrimary)
|
|
174
203
|
continue;
|
|
175
204
|
existing.request = m.request;
|
|
@@ -191,7 +220,7 @@ const networkSearch = defineTool({
|
|
|
191
220
|
const topMatches = aggregatedMatches.slice(0, MAX_SEARCH_RESULTS);
|
|
192
221
|
const structured = {
|
|
193
222
|
keyword: params.keyword,
|
|
194
|
-
requests: topMatches.map(m => {
|
|
223
|
+
requests: topMatches.map((m) => {
|
|
195
224
|
const method = m.request.method().toUpperCase();
|
|
196
225
|
const url = m.request.url();
|
|
197
226
|
// Normalize URL for grouping only; not returned in output
|
|
@@ -212,9 +241,10 @@ const networkSearch = defineTool({
|
|
|
212
241
|
})();
|
|
213
242
|
const primaryGroups = m.primaryGroups
|
|
214
243
|
.slice()
|
|
215
|
-
.sort((a, b) => b.count - a.count ||
|
|
244
|
+
.sort((a, b) => b.count - a.count ||
|
|
245
|
+
getDepth(b.normalized) - getDepth(a.normalized))
|
|
216
246
|
.slice(0, MAX_GROUPS_TO_SHOW);
|
|
217
|
-
const paths = primaryGroups.map(group => ({
|
|
247
|
+
const paths = primaryGroups.map((group) => ({
|
|
218
248
|
path: formatGroupPath(group),
|
|
219
249
|
examples: extractExamples(group),
|
|
220
250
|
}));
|
|
@@ -236,7 +266,7 @@ const networkSearch = defineTool({
|
|
|
236
266
|
params: queryKeys.slice(0, 6),
|
|
237
267
|
paths,
|
|
238
268
|
};
|
|
239
|
-
})
|
|
269
|
+
}),
|
|
240
270
|
};
|
|
241
271
|
response.addResult(JSON.stringify(structured));
|
|
242
272
|
}
|