@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,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { memoize, pipe } from "@fxts/core";
|
|
2
|
+
import { alt_sc, apply, buildLexer, extractByTokenRange, kmid, makeParserModule, rep_sc, seq, tok, } from "typescript-parsec";
|
|
3
3
|
// Pure utility functions
|
|
4
|
-
export const escapeString = (str) => str.replace(/\\/g,
|
|
5
|
-
const isBlank = (str) => str.trim() ===
|
|
6
|
-
export const createHelperCall = (helper, ...args) => `__pwHelpers.${helper}(${args.map(a => `'${escapeString(a)}'`).join(
|
|
4
|
+
export const escapeString = (str) => str.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
5
|
+
const isBlank = (str) => str.trim() === "";
|
|
6
|
+
export const createHelperCall = (helper, ...args) => `__pwHelpers.${helper}(${args.map((a) => `'${escapeString(a)}'`).join(", ")})`;
|
|
7
7
|
var SelTokKind;
|
|
8
8
|
(function (SelTokKind) {
|
|
9
9
|
SelTokKind[SelTokKind["HasText"] = 0] = "HasText";
|
|
@@ -36,49 +36,49 @@ const selectorLexer = buildLexer([
|
|
|
36
36
|
[true, /^"(?:[^"\\]|\\.)*"/g, SelTokKind.DQString],
|
|
37
37
|
[true, /^'(?:[^'\\]|\\.)*'/g, SelTokKind.SQString],
|
|
38
38
|
[true, /^`(?:[^`\\]|\\.)*`/g, SelTokKind.BQString],
|
|
39
|
-
[true, /^\/(?:[
|
|
40
|
-
[true, /^[
|
|
39
|
+
[true, /^\/(?:[^/\\]|\\.)+\/[a-z]*/gi, SelTokKind.Regex],
|
|
40
|
+
[true, /^[\s\S]/g, SelTokKind.Other],
|
|
41
41
|
]);
|
|
42
42
|
// text args can be "..." or '...' or `...` or /.../flags
|
|
43
|
-
const TEXT_ARG = alt_sc(apply(tok(SelTokKind.DQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.SQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.BQString), t => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.Regex), t => t.text));
|
|
43
|
+
const TEXT_ARG = alt_sc(apply(tok(SelTokKind.DQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.SQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.BQString), (t) => t.text.slice(1, t.text.length - 1)), apply(tok(SelTokKind.Regex), (t) => t.text));
|
|
44
44
|
// Keep for generic quoted (used where we want the raw quoted text, not regex)
|
|
45
45
|
const QUOTED = alt_sc(tok(SelTokKind.DQString), tok(SelTokKind.SQString), tok(SelTokKind.BQString));
|
|
46
46
|
// Recursive string parser for :has(...) argument
|
|
47
47
|
const HasArgModule = makeParserModule({
|
|
48
|
-
ITEM: m => alt_sc(apply(QUOTED, t => t.text), apply(tok(SelTokKind.Regex), t => t.text), // allow regex inside :has inner
|
|
49
|
-
apply(tok(SelTokKind.Has), t => t.text), apply(tok(SelTokKind.HasText), t => t.text), apply(tok(SelTokKind.Contains), t => t.text), apply(tok(SelTokKind.Text), t => t.text), apply(tok(SelTokKind.Visible), t => t.text), apply(tok(SelTokKind.NthMatch), t => t.text), apply(tok(SelTokKind.Comma), t => t.text), // allow comma in inner (selector lists)
|
|
50
|
-
apply(tok(SelTokKind.Other), t => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), v =>
|
|
51
|
-
ARG: m => apply(rep_sc(m.ITEM), arr => arr.join(
|
|
48
|
+
ITEM: (m) => alt_sc(apply(QUOTED, (t) => t.text), apply(tok(SelTokKind.Regex), (t) => t.text), // allow regex inside :has inner
|
|
49
|
+
apply(tok(SelTokKind.Has), (t) => t.text), apply(tok(SelTokKind.HasText), (t) => t.text), apply(tok(SelTokKind.Contains), (t) => t.text), apply(tok(SelTokKind.Text), (t) => t.text), apply(tok(SelTokKind.Visible), (t) => t.text), apply(tok(SelTokKind.NthMatch), (t) => t.text), apply(tok(SelTokKind.Comma), (t) => t.text), // allow comma in inner (selector lists)
|
|
50
|
+
apply(tok(SelTokKind.Other), (t) => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), (v) => "(" + v[1] + ")")),
|
|
51
|
+
ARG: (m) => apply(rep_sc(m.ITEM), (arr) => arr.join("")),
|
|
52
52
|
});
|
|
53
53
|
// first arg of :nth-match — same as ARG but *without* comma tokens
|
|
54
54
|
const HasArgNoCommaModule = makeParserModule({
|
|
55
|
-
ITEM: m => alt_sc(apply(QUOTED, t => t.text), apply(tok(SelTokKind.Regex), t => t.text), apply(tok(SelTokKind.Has), t => t.text), apply(tok(SelTokKind.HasText), t => t.text), apply(tok(SelTokKind.Contains), t => t.text), apply(tok(SelTokKind.Text), t => t.text), apply(tok(SelTokKind.Visible), t => t.text), apply(tok(SelTokKind.NthMatch), t => t.text), apply(tok(SelTokKind.Other), t => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), v =>
|
|
56
|
-
ARG: m => apply(rep_sc(m.ITEM), arr => arr.join(
|
|
55
|
+
ITEM: (m) => alt_sc(apply(QUOTED, (t) => t.text), apply(tok(SelTokKind.Regex), (t) => t.text), apply(tok(SelTokKind.Has), (t) => t.text), apply(tok(SelTokKind.HasText), (t) => t.text), apply(tok(SelTokKind.Contains), (t) => t.text), apply(tok(SelTokKind.Text), (t) => t.text), apply(tok(SelTokKind.Visible), (t) => t.text), apply(tok(SelTokKind.NthMatch), (t) => t.text), apply(tok(SelTokKind.Other), (t) => t.text), apply(seq(tok(SelTokKind.LParen), m.ARG, tok(SelTokKind.RParen)), (v) => "(" + v[1] + ")")),
|
|
56
|
+
ARG: (m) => apply(rep_sc(m.ITEM), (arr) => arr.join("")),
|
|
57
57
|
});
|
|
58
58
|
const HAS_ARG = HasArgModule.ARG;
|
|
59
59
|
const NTH_ARG_NO_COMMA = HasArgNoCommaModule.ARG;
|
|
60
60
|
const PseudoModule = makeParserModule({
|
|
61
|
-
HAS: () => apply(kmid(seq(tok(SelTokKind.Has), tok(SelTokKind.LParen)), HAS_ARG, tok(SelTokKind.RParen)), inner => ({ kind:
|
|
62
|
-
HAS_TEXT: () => apply(kmid(seq(tok(SelTokKind.HasText), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind:
|
|
63
|
-
CONTAINS: () => apply(kmid(seq(tok(SelTokKind.Contains), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind:
|
|
64
|
-
TEXT: () => apply(kmid(seq(tok(SelTokKind.Text), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), text => ({ kind:
|
|
65
|
-
VISIBLE: () => apply(tok(SelTokKind.Visible), () => ({ kind:
|
|
61
|
+
HAS: () => apply(kmid(seq(tok(SelTokKind.Has), tok(SelTokKind.LParen)), HAS_ARG, tok(SelTokKind.RParen)), (inner) => ({ kind: "has", inner })),
|
|
62
|
+
HAS_TEXT: () => apply(kmid(seq(tok(SelTokKind.HasText), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "hasText", text })),
|
|
63
|
+
CONTAINS: () => apply(kmid(seq(tok(SelTokKind.Contains), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "contains", text })),
|
|
64
|
+
TEXT: () => apply(kmid(seq(tok(SelTokKind.Text), tok(SelTokKind.LParen)), TEXT_ARG, tok(SelTokKind.RParen)), (text) => ({ kind: "text", text })),
|
|
65
|
+
VISIBLE: () => apply(tok(SelTokKind.Visible), () => ({ kind: "visible" })),
|
|
66
66
|
// :nth-match(<selector>, <index>)
|
|
67
67
|
NTH_MATCH: () => apply(kmid(seq(tok(SelTokKind.NthMatch), tok(SelTokKind.LParen)), seq(NTH_ARG_NO_COMMA, // first arg string (no commas)
|
|
68
|
-
apply(rep_sc(tok(SelTokKind.WS)), () =>
|
|
69
|
-
tok(SelTokKind.Comma), apply(rep_sc(tok(SelTokKind.WS)), () =>
|
|
70
|
-
TEXT_ARG), tok(SelTokKind.RParen)), v => {
|
|
68
|
+
apply(rep_sc(tok(SelTokKind.WS)), () => ""), // optional spaces
|
|
69
|
+
tok(SelTokKind.Comma), apply(rep_sc(tok(SelTokKind.WS)), () => ""), // optional spaces
|
|
70
|
+
TEXT_ARG), tok(SelTokKind.RParen)), (v) => {
|
|
71
71
|
const inner = v[0];
|
|
72
72
|
const idxRaw = v[4];
|
|
73
|
-
const index = parseInt(idxRaw, 10);
|
|
74
|
-
return { kind:
|
|
73
|
+
const index = Number.parseInt(idxRaw, 10);
|
|
74
|
+
return { kind: "nthMatch", inner, index: isFinite(index) ? index : 1 };
|
|
75
75
|
}),
|
|
76
|
-
ANY: m => alt_sc(m.HAS, m.HAS_TEXT, m.CONTAINS, m.TEXT, m.VISIBLE, m.NTH_MATCH),
|
|
76
|
+
ANY: (m) => alt_sc(m.HAS, m.HAS_TEXT, m.CONTAINS, m.TEXT, m.VISIBLE, m.NTH_MATCH),
|
|
77
77
|
});
|
|
78
78
|
const parseFirstPseudo = (selector) => {
|
|
79
79
|
const start = selectorLexer.parse(selector);
|
|
80
80
|
if (!start)
|
|
81
|
-
return { kind:
|
|
81
|
+
return { kind: "none" };
|
|
82
82
|
let t = start;
|
|
83
83
|
while (t) {
|
|
84
84
|
const parsed = PseudoModule.ANY.parse(t);
|
|
@@ -88,21 +88,21 @@ const parseFirstPseudo = (selector) => {
|
|
|
88
88
|
const prefix = selector.slice(0, t.pos.index);
|
|
89
89
|
const base = prefix + selector.slice(t.pos.index + consumed);
|
|
90
90
|
const res = cand.result;
|
|
91
|
-
if (res.kind ===
|
|
92
|
-
return { kind:
|
|
93
|
-
if (res.kind ===
|
|
94
|
-
return { kind:
|
|
95
|
-
if (res.kind ===
|
|
96
|
-
return { kind:
|
|
97
|
-
if (res.kind ===
|
|
98
|
-
return { kind:
|
|
99
|
-
if (res.kind ===
|
|
100
|
-
return { kind:
|
|
101
|
-
return { kind:
|
|
91
|
+
if (res.kind === "has")
|
|
92
|
+
return { kind: "has", base, inner: res.inner };
|
|
93
|
+
if (res.kind === "visible")
|
|
94
|
+
return { kind: "visible", base };
|
|
95
|
+
if (res.kind === "hasText")
|
|
96
|
+
return { kind: "hasText", base, text: res.text };
|
|
97
|
+
if (res.kind === "contains")
|
|
98
|
+
return { kind: "contains", base, text: res.text };
|
|
99
|
+
if (res.kind === "nthMatch")
|
|
100
|
+
return { kind: "nthMatch", base, inner: res.inner, index: res.index };
|
|
101
|
+
return { kind: "text", base, text: res.text };
|
|
102
102
|
}
|
|
103
103
|
t = t.next;
|
|
104
104
|
}
|
|
105
|
-
return { kind:
|
|
105
|
+
return { kind: "none" };
|
|
106
106
|
};
|
|
107
107
|
var SkipTokKind;
|
|
108
108
|
(function (SkipTokKind) {
|
|
@@ -122,7 +122,7 @@ var SkipTokKind;
|
|
|
122
122
|
})(SkipTokKind || (SkipTokKind = {}));
|
|
123
123
|
const skipLexer = buildLexer([
|
|
124
124
|
[true, /^\/\/[^\n]*/g, SkipTokKind.LineComment],
|
|
125
|
-
[true, /^\/\*[
|
|
125
|
+
[true, /^\/\*[\s\S]*?\*\//g, SkipTokKind.BlockComment],
|
|
126
126
|
[true, /^"(?:[^"\\]|\\.)*"/g, SkipTokKind.DQString],
|
|
127
127
|
[true, /^'(?:[^'\\]|\\.)*'/g, SkipTokKind.SQString],
|
|
128
128
|
[true, /^`(?:[^`\\]|\\.)*`/g, SkipTokKind.BQString],
|
|
@@ -133,39 +133,57 @@ const skipLexer = buildLexer([
|
|
|
133
133
|
[true, /^querySelectorAll\b/g, SkipTokKind.QuerySelectorAll],
|
|
134
134
|
[true, /^querySelector\b/g, SkipTokKind.QuerySelector],
|
|
135
135
|
[true, /^[A-Za-z_$][A-Za-z0-9_$]*/g, SkipTokKind.Identifier],
|
|
136
|
-
[true, /^[
|
|
136
|
+
[true, /^[\s\S]/g, SkipTokKind.Other],
|
|
137
137
|
]);
|
|
138
138
|
const SKIP_CALL_QUOTED = alt_sc(tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString));
|
|
139
139
|
// Zero-or-more space/comments -> string
|
|
140
140
|
const WS_OR_COMMENT = alt_sc(tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment));
|
|
141
|
-
const SP0 = apply(rep_sc(WS_OR_COMMENT), ts => ts.map(t => t.text).join(
|
|
141
|
+
const SP0 = apply(rep_sc(WS_OR_COMMENT), (ts) => ts.map((t) => t.text).join(""));
|
|
142
142
|
// receiver := Identifier ('.' Identifier)*
|
|
143
143
|
const RECEIVER = apply(seq(tok(SkipTokKind.Identifier), rep_sc(seq(tok(SkipTokKind.Dot), tok(SkipTokKind.Identifier)))), (v) => {
|
|
144
144
|
const head = v[0].text;
|
|
145
|
-
const tail = v[1]
|
|
145
|
+
const tail = v[1]
|
|
146
|
+
.map((p) => p[0].text + p[1].text)
|
|
147
|
+
.join("");
|
|
146
148
|
return head + tail;
|
|
147
149
|
});
|
|
148
150
|
const SegmentModule = makeParserModule({
|
|
149
|
-
RAW: () => apply(alt_sc(tok(SkipTokKind.LParen), tok(SkipTokKind.RParen), tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment), tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString), tok(SkipTokKind.QuerySelectorAll), tok(SkipTokKind.QuerySelector), tok(SkipTokKind.Identifier), tok(SkipTokKind.Dot), tok(SkipTokKind.Other)), t => ({ kind:
|
|
150
|
-
CALL_ONE: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelector), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), v => {
|
|
151
|
+
RAW: () => apply(alt_sc(tok(SkipTokKind.LParen), tok(SkipTokKind.RParen), tok(SkipTokKind.WS), tok(SkipTokKind.LineComment), tok(SkipTokKind.BlockComment), tok(SkipTokKind.DQString), tok(SkipTokKind.SQString), tok(SkipTokKind.BQString), tok(SkipTokKind.QuerySelectorAll), tok(SkipTokKind.QuerySelector), tok(SkipTokKind.Identifier), tok(SkipTokKind.Dot), tok(SkipTokKind.Other)), (t) => ({ kind: "raw", text: t.text })),
|
|
152
|
+
CALL_ONE: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelector), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), (v) => {
|
|
151
153
|
const lit = v[6];
|
|
152
154
|
return {
|
|
153
|
-
kind:
|
|
154
|
-
method:
|
|
155
|
+
kind: "call",
|
|
156
|
+
method: "one",
|
|
155
157
|
selector: lit.text.slice(1, lit.text.length - 1),
|
|
156
|
-
original: v[0] +
|
|
158
|
+
original: v[0] +
|
|
159
|
+
v[1].text +
|
|
160
|
+
v[2].text +
|
|
161
|
+
v[3] +
|
|
162
|
+
v[4].text +
|
|
163
|
+
v[5] +
|
|
164
|
+
v[6].text +
|
|
165
|
+
v[7] +
|
|
166
|
+
v[8].text,
|
|
157
167
|
};
|
|
158
168
|
}),
|
|
159
|
-
CALL_ALL: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelectorAll), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), v => {
|
|
169
|
+
CALL_ALL: () => apply(seq(RECEIVER, tok(SkipTokKind.Dot), tok(SkipTokKind.QuerySelectorAll), SP0, tok(SkipTokKind.LParen), SP0, SKIP_CALL_QUOTED, SP0, tok(SkipTokKind.RParen)), (v) => {
|
|
160
170
|
const lit = v[6];
|
|
161
171
|
return {
|
|
162
|
-
kind:
|
|
163
|
-
method:
|
|
172
|
+
kind: "call",
|
|
173
|
+
method: "all",
|
|
164
174
|
selector: lit.text.slice(1, lit.text.length - 1),
|
|
165
|
-
original: v[0] +
|
|
175
|
+
original: v[0] +
|
|
176
|
+
v[1].text +
|
|
177
|
+
v[2].text +
|
|
178
|
+
v[3] +
|
|
179
|
+
v[4].text +
|
|
180
|
+
v[5] +
|
|
181
|
+
v[6].text +
|
|
182
|
+
v[7] +
|
|
183
|
+
v[8].text,
|
|
166
184
|
};
|
|
167
185
|
}),
|
|
168
|
-
SEGMENT: m => alt_sc(m.CALL_ALL, m.CALL_ONE, m.RAW),
|
|
186
|
+
SEGMENT: (m) => alt_sc(m.CALL_ALL, m.CALL_ONE, m.RAW),
|
|
169
187
|
});
|
|
170
188
|
const PROGRAM = rep_sc(SegmentModule.SEGMENT);
|
|
171
189
|
// Helper functions template
|
|
@@ -286,21 +304,21 @@ export const helperFunctions = `
|
|
|
286
304
|
`;
|
|
287
305
|
// ----- Transformers -----
|
|
288
306
|
export const transformHasText = ({ script, needsHelpers }) => {
|
|
289
|
-
if (!script.includes(
|
|
307
|
+
if (!script.includes(":has-text("))
|
|
290
308
|
return { script, needsHelpers };
|
|
291
309
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
292
310
|
const parsed = parseFirstPseudo(selector);
|
|
293
|
-
if (parsed.kind !==
|
|
311
|
+
if (parsed.kind !== "hasText")
|
|
294
312
|
return null;
|
|
295
313
|
// if base is empty, behave like :text() — search globally
|
|
296
314
|
if (isBlank(parsed.base)) {
|
|
297
|
-
return method ===
|
|
298
|
-
? createHelperCall(
|
|
299
|
-
: createHelperCall(
|
|
315
|
+
return method === "all"
|
|
316
|
+
? createHelperCall("hasTextAll", "*", parsed.text)
|
|
317
|
+
: createHelperCall("text", parsed.text);
|
|
300
318
|
}
|
|
301
|
-
return method ===
|
|
302
|
-
? createHelperCall(
|
|
303
|
-
: createHelperCall(
|
|
319
|
+
return method === "all"
|
|
320
|
+
? createHelperCall("hasTextAll", parsed.base, parsed.text)
|
|
321
|
+
: createHelperCall("hasText", parsed.base, parsed.text);
|
|
304
322
|
});
|
|
305
323
|
return {
|
|
306
324
|
script: result.output,
|
|
@@ -308,15 +326,15 @@ export const transformHasText = ({ script, needsHelpers }) => {
|
|
|
308
326
|
};
|
|
309
327
|
};
|
|
310
328
|
export const transformVisible = ({ script, needsHelpers }) => {
|
|
311
|
-
if (!script.includes(
|
|
329
|
+
if (!script.includes(":visible"))
|
|
312
330
|
return { script, needsHelpers };
|
|
313
331
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
314
332
|
const parsed = parseFirstPseudo(selector);
|
|
315
|
-
if (parsed.kind !==
|
|
333
|
+
if (parsed.kind !== "visible")
|
|
316
334
|
return null;
|
|
317
|
-
return method ===
|
|
318
|
-
? createHelperCall(
|
|
319
|
-
: createHelperCall(
|
|
335
|
+
return method === "all"
|
|
336
|
+
? createHelperCall("visibleAll", parsed.base)
|
|
337
|
+
: createHelperCall("visible", parsed.base);
|
|
320
338
|
});
|
|
321
339
|
return {
|
|
322
340
|
script: result.output,
|
|
@@ -324,20 +342,20 @@ export const transformVisible = ({ script, needsHelpers }) => {
|
|
|
324
342
|
};
|
|
325
343
|
};
|
|
326
344
|
export const transformText = ({ script, needsHelpers }) => {
|
|
327
|
-
if (!script.includes(
|
|
345
|
+
if (!script.includes(":text("))
|
|
328
346
|
return { script, needsHelpers };
|
|
329
347
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
330
348
|
const parsed = parseFirstPseudo(selector);
|
|
331
|
-
if (parsed.kind !==
|
|
349
|
+
if (parsed.kind !== "text")
|
|
332
350
|
return null;
|
|
333
351
|
if (isBlank(parsed.base)) {
|
|
334
|
-
return method ===
|
|
335
|
-
? createHelperCall(
|
|
336
|
-
: createHelperCall(
|
|
352
|
+
return method === "all"
|
|
353
|
+
? createHelperCall("hasTextAll", "*", parsed.text)
|
|
354
|
+
: createHelperCall("text", parsed.text);
|
|
337
355
|
}
|
|
338
|
-
return method ===
|
|
339
|
-
? createHelperCall(
|
|
340
|
-
: createHelperCall(
|
|
356
|
+
return method === "all"
|
|
357
|
+
? createHelperCall("hasTextAll", parsed.base, parsed.text)
|
|
358
|
+
: createHelperCall("hasText", parsed.base, parsed.text);
|
|
341
359
|
});
|
|
342
360
|
return {
|
|
343
361
|
script: result.output,
|
|
@@ -353,42 +371,47 @@ export const transformTextEngine = ({ script, needsHelpers }) => {
|
|
|
353
371
|
if (m1) {
|
|
354
372
|
const arg = m1[1].trim();
|
|
355
373
|
const val = unwrapPossibleQuotesOrKeepRegex(arg);
|
|
356
|
-
return method ===
|
|
357
|
-
? createHelperCall(
|
|
358
|
-
: createHelperCall(
|
|
374
|
+
return method === "all"
|
|
375
|
+
? createHelperCall("textAll", val)
|
|
376
|
+
: createHelperCall("text", val);
|
|
359
377
|
}
|
|
360
378
|
// Case 2: X >> text=...
|
|
361
379
|
const m2 = raw.match(/^(.*?)>>\s*text\s*=\s*(.+)\s*$/);
|
|
362
380
|
if (m2) {
|
|
363
|
-
const base = m2[1].trim() ||
|
|
381
|
+
const base = m2[1].trim() || "*";
|
|
364
382
|
const arg = m2[2].trim();
|
|
365
383
|
const val = unwrapPossibleQuotesOrKeepRegex(arg);
|
|
366
|
-
return method ===
|
|
367
|
-
? createHelperCall(
|
|
368
|
-
: createHelperCall(
|
|
384
|
+
return method === "all"
|
|
385
|
+
? createHelperCall("hasTextAll", base, val)
|
|
386
|
+
: createHelperCall("hasText", base, val);
|
|
369
387
|
}
|
|
370
388
|
return null;
|
|
371
389
|
});
|
|
372
|
-
return {
|
|
390
|
+
return {
|
|
391
|
+
script: result.output,
|
|
392
|
+
needsHelpers: needsHelpers || result.changed,
|
|
393
|
+
};
|
|
373
394
|
};
|
|
374
395
|
// Utility used only by transformTextEngine
|
|
375
396
|
const unwrapPossibleQuotesOrKeepRegex = (s) => {
|
|
376
397
|
const q = s[0];
|
|
377
|
-
if ((q === '"' || q === "'" || q ===
|
|
398
|
+
if ((q === '"' || q === "'" || q === "`") &&
|
|
399
|
+
s.length >= 2 &&
|
|
400
|
+
s[s.length - 1] === q)
|
|
378
401
|
return s.slice(1, -1);
|
|
379
402
|
// keep /.../flags as-is
|
|
380
403
|
return s;
|
|
381
404
|
};
|
|
382
405
|
export const transformContains = ({ script, needsHelpers }) => {
|
|
383
|
-
if (!script.includes(
|
|
406
|
+
if (!script.includes(":contains("))
|
|
384
407
|
return { script, needsHelpers };
|
|
385
408
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
386
409
|
const parsed = parseFirstPseudo(selector);
|
|
387
|
-
if (parsed.kind !==
|
|
410
|
+
if (parsed.kind !== "contains")
|
|
388
411
|
return null;
|
|
389
|
-
return method ===
|
|
390
|
-
? createHelperCall(
|
|
391
|
-
: createHelperCall(
|
|
412
|
+
return method === "all"
|
|
413
|
+
? createHelperCall("hasTextAll", parsed.base, parsed.text)
|
|
414
|
+
: createHelperCall("hasText", parsed.base, parsed.text);
|
|
392
415
|
});
|
|
393
416
|
return {
|
|
394
417
|
script: result.output,
|
|
@@ -396,15 +419,15 @@ export const transformContains = ({ script, needsHelpers }) => {
|
|
|
396
419
|
};
|
|
397
420
|
};
|
|
398
421
|
export const transformHas = ({ script, needsHelpers }) => {
|
|
399
|
-
if (!script.includes(
|
|
422
|
+
if (!script.includes(":has("))
|
|
400
423
|
return { script, needsHelpers };
|
|
401
424
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
402
425
|
const parsed = parseFirstPseudo(selector);
|
|
403
|
-
if (parsed.kind !==
|
|
426
|
+
if (parsed.kind !== "has")
|
|
404
427
|
return null;
|
|
405
|
-
return method ===
|
|
406
|
-
? createHelperCall(
|
|
407
|
-
: createHelperCall(
|
|
428
|
+
return method === "all"
|
|
429
|
+
? createHelperCall("hasAll", parsed.base, parsed.inner)
|
|
430
|
+
: createHelperCall("has", parsed.base, parsed.inner);
|
|
408
431
|
});
|
|
409
432
|
return {
|
|
410
433
|
script: result.output,
|
|
@@ -413,17 +436,17 @@ export const transformHas = ({ script, needsHelpers }) => {
|
|
|
413
436
|
};
|
|
414
437
|
// :nth-match(...)
|
|
415
438
|
export const transformNthMatch = ({ script, needsHelpers }) => {
|
|
416
|
-
if (!script.includes(
|
|
439
|
+
if (!script.includes(":nth-match("))
|
|
417
440
|
return { script, needsHelpers };
|
|
418
441
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
419
442
|
const parsed = parseFirstPseudo(selector);
|
|
420
|
-
if (parsed.kind !==
|
|
443
|
+
if (parsed.kind !== "nthMatch")
|
|
421
444
|
return null;
|
|
422
445
|
// If base is empty, treat as global
|
|
423
|
-
const base = (parsed.base ||
|
|
424
|
-
if (method ===
|
|
425
|
-
return createHelperCall(
|
|
426
|
-
return createHelperCall(
|
|
446
|
+
const base = (parsed.base || "").trim();
|
|
447
|
+
if (method === "all")
|
|
448
|
+
return createHelperCall("nthMatchAll", base, parsed.inner, String(parsed.index));
|
|
449
|
+
return createHelperCall("nthMatch", base, parsed.inner, String(parsed.index));
|
|
427
450
|
});
|
|
428
451
|
return {
|
|
429
452
|
script: result.output,
|
|
@@ -431,14 +454,14 @@ export const transformNthMatch = ({ script, needsHelpers }) => {
|
|
|
431
454
|
};
|
|
432
455
|
};
|
|
433
456
|
export const transformDescendant = ({ script, needsHelpers }) => {
|
|
434
|
-
if (!script.includes(
|
|
457
|
+
if (!script.includes(">>"))
|
|
435
458
|
return { script, needsHelpers };
|
|
436
459
|
const result = transformSelectorCalls(script, (selector, method) => {
|
|
437
|
-
if (!selector.includes(
|
|
460
|
+
if (!selector.includes(">>"))
|
|
438
461
|
return null;
|
|
439
|
-
return method ===
|
|
440
|
-
? createHelperCall(
|
|
441
|
-
: createHelperCall(
|
|
462
|
+
return method === "all"
|
|
463
|
+
? createHelperCall("querySelectorAll", selector)
|
|
464
|
+
: createHelperCall("querySelector", selector);
|
|
442
465
|
});
|
|
443
466
|
return {
|
|
444
467
|
script: result.output,
|
|
@@ -449,7 +472,7 @@ export const transformDescendant = ({ script, needsHelpers }) => {
|
|
|
449
472
|
export const injectHelpers = ({ script, needsHelpers }) => needsHelpers
|
|
450
473
|
? {
|
|
451
474
|
script: helperFunctions +
|
|
452
|
-
|
|
475
|
+
"\n/* Playwright selectors automatically transformed */\n" +
|
|
453
476
|
script,
|
|
454
477
|
needsHelpers,
|
|
455
478
|
}
|
|
@@ -479,7 +502,7 @@ const transformSelectorCalls = (code, rewriter) => {
|
|
|
479
502
|
let changed = false;
|
|
480
503
|
const out = [];
|
|
481
504
|
for (const seg of segments) {
|
|
482
|
-
if (seg.kind ===
|
|
505
|
+
if (seg.kind === "call") {
|
|
483
506
|
const replacement = rewriter(seg.selector, seg.method);
|
|
484
507
|
if (replacement) {
|
|
485
508
|
out.push(replacement);
|
|
@@ -493,5 +516,5 @@ const transformSelectorCalls = (code, rewriter) => {
|
|
|
493
516
|
out.push(seg.text);
|
|
494
517
|
}
|
|
495
518
|
}
|
|
496
|
-
return { output: out.join(
|
|
519
|
+
return { output: out.join(""), changed };
|
|
497
520
|
};
|
package/lib/program.js
CHANGED
|
@@ -13,59 +13,59 @@
|
|
|
13
13
|
* See the License for the specific language governing permissions and
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList } from
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { ProxyBackend } from
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
16
|
+
import { Option, program } from "commander";
|
|
17
|
+
import { contextFactory } from "./browserContextFactory.js";
|
|
18
|
+
import { BrowserServerBackend } from "./browserServerBackend.js";
|
|
19
|
+
import { commaSeparatedList, resolveCLIConfig, semicolonSeparatedList, } from "./config.js";
|
|
20
|
+
import { Context } from "./context.js";
|
|
21
|
+
import { ExtensionContextFactory } from "./extension/extensionContextFactory.js";
|
|
22
|
+
import { InProcessTransport } from "./mcp/inProcessTransport.js";
|
|
23
|
+
import { ProxyBackend } from "./mcp/proxyBackend.js";
|
|
24
|
+
import * as mcpServer from "./mcp/server.js";
|
|
25
|
+
import * as mcpTransport from "./mcp/transport.js";
|
|
26
|
+
import { packageJSON } from "./utils/package.js";
|
|
27
27
|
program
|
|
28
|
-
.version(
|
|
28
|
+
.version("Version " + packageJSON.version)
|
|
29
29
|
.name(packageJSON.name)
|
|
30
|
-
.option(
|
|
31
|
-
.option(
|
|
32
|
-
.option(
|
|
33
|
-
.option(
|
|
34
|
-
.option(
|
|
35
|
-
.option(
|
|
36
|
-
.option(
|
|
37
|
-
.option(
|
|
38
|
-
.option(
|
|
39
|
-
.option(
|
|
40
|
-
.option(
|
|
41
|
-
.option(
|
|
42
|
-
.option(
|
|
43
|
-
.option(
|
|
44
|
-
.option(
|
|
45
|
-
.option(
|
|
46
|
-
.option(
|
|
47
|
-
.option(
|
|
48
|
-
.option(
|
|
49
|
-
.option(
|
|
50
|
-
.option(
|
|
51
|
-
.option(
|
|
52
|
-
.option(
|
|
53
|
-
.option(
|
|
54
|
-
.option(
|
|
55
|
-
.option(
|
|
56
|
-
.option(
|
|
57
|
-
.option(
|
|
58
|
-
.option(
|
|
59
|
-
.addOption(new Option(
|
|
60
|
-
.addOption(new Option(
|
|
61
|
-
.addOption(new Option(
|
|
62
|
-
.addOption(new Option(
|
|
30
|
+
.option("--allowed-origins <origins>", "semicolon-separated list of origins to allow the browser to request. Default is to allow all.", semicolonSeparatedList)
|
|
31
|
+
.option("--blocked-origins <origins>", "semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed.", semicolonSeparatedList)
|
|
32
|
+
.option("--block-service-workers", "block service workers")
|
|
33
|
+
.option("--browser <browser>", "browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.")
|
|
34
|
+
.option("--caps <caps>", "comma-separated list of additional capabilities to enable, possible values: vision, pdf.", commaSeparatedList)
|
|
35
|
+
.option("--cdp-endpoint <endpoint>", "CDP endpoint to connect to.")
|
|
36
|
+
.option("--config <path>", "path to the configuration file.")
|
|
37
|
+
.option("--device <device>", 'device to emulate, for example: "iPhone 15"')
|
|
38
|
+
.option("--executable-path <path>", "path to the browser executable.")
|
|
39
|
+
.option("--headless", "run browser in headless mode, headed by default")
|
|
40
|
+
.option("--host <host>", "host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.")
|
|
41
|
+
.option("--ignore-https-errors", "ignore https errors")
|
|
42
|
+
.option("--init-script <path>", "path to a JavaScript file to inject into all pages using addInitScript.")
|
|
43
|
+
.option("--isolated", "keep the browser profile in memory, do not save it to disk.")
|
|
44
|
+
.option("--image-responses <mode>", 'whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".')
|
|
45
|
+
.option("--no-sandbox", "disable the sandbox for all process types that are normally sandboxed.")
|
|
46
|
+
.option("--output-dir <path>", "path to the directory for output files.")
|
|
47
|
+
.option("--port <port>", "port to listen on for SSE transport.")
|
|
48
|
+
.option("--proxy-bypass <bypass>", 'comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"')
|
|
49
|
+
.option("--proxy-server <proxy>", 'specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"')
|
|
50
|
+
.option("--save-session", "Whether to save the Playwright MCP session into the output directory.")
|
|
51
|
+
.option("--save-trace", "Whether to save the Playwright Trace of the session into the output directory.")
|
|
52
|
+
.option("--storage-state <path>", "path to the storage state file for isolated sessions.")
|
|
53
|
+
.option("--user-agent <ua string>", "specify user agent string")
|
|
54
|
+
.option("--user-data-dir <path>", "path to the user data directory. If not specified, a temporary directory will be created.")
|
|
55
|
+
.option("--viewport-size <size>", 'specify browser viewport size in pixels, for example "1280, 720"')
|
|
56
|
+
.option("--window-position <x,y>", 'specify Chrome window position in pixels, for example "100,200"')
|
|
57
|
+
.option("--window-size <width,height>", 'specify Chrome window size in pixels, for example "1280,720"')
|
|
58
|
+
.option("--app <url>", "launch browser in app mode with the specified URL")
|
|
59
|
+
.addOption(new Option("--extension", 'Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright MCP Bridge" browser extension to be installed.').hideHelp())
|
|
60
|
+
.addOption(new Option("--connect-tool", "Allow to switch between different browser connection methods.").hideHelp())
|
|
61
|
+
.addOption(new Option("--loop-tools", "Run loop tools").hideHelp())
|
|
62
|
+
.addOption(new Option("--vision", "Legacy option, use --caps=vision instead").hideHelp())
|
|
63
63
|
.action(async (options) => {
|
|
64
64
|
setupExitWatchdog();
|
|
65
65
|
if (options.vision) {
|
|
66
66
|
// eslint-disable-next-line no-console
|
|
67
|
-
console.error(
|
|
68
|
-
options.caps =
|
|
67
|
+
console.error("The --vision option is deprecated, use --caps=vision instead");
|
|
68
|
+
options.caps = "vision";
|
|
69
69
|
}
|
|
70
70
|
const config = await resolveCLIConfig(options);
|
|
71
71
|
if (options.extension) {
|
|
@@ -75,7 +75,9 @@ program
|
|
|
75
75
|
return;
|
|
76
76
|
}
|
|
77
77
|
const browserContextFactory = contextFactory(config);
|
|
78
|
-
const providers = [
|
|
78
|
+
const providers = [
|
|
79
|
+
mcpProviderForBrowserContextFactory(config, browserContextFactory),
|
|
80
|
+
];
|
|
79
81
|
if (options.connectTool)
|
|
80
82
|
providers.push(mcpProviderForBrowserContextFactory(config, createExtensionContextFactory(config)));
|
|
81
83
|
await mcpTransport.start(() => new ProxyBackend(providers), config.server);
|
|
@@ -90,12 +92,12 @@ function setupExitWatchdog() {
|
|
|
90
92
|
await Context.disposeAll();
|
|
91
93
|
process.exit(0);
|
|
92
94
|
};
|
|
93
|
-
process.stdin.on(
|
|
94
|
-
process.on(
|
|
95
|
-
process.on(
|
|
95
|
+
process.stdin.on("close", handleExit);
|
|
96
|
+
process.on("SIGINT", handleExit);
|
|
97
|
+
process.on("SIGTERM", handleExit);
|
|
96
98
|
}
|
|
97
99
|
function createExtensionContextFactory(config) {
|
|
98
|
-
return new ExtensionContextFactory(config.browser.launchOptions.channel ||
|
|
100
|
+
return new ExtensionContextFactory(config.browser.launchOptions.channel || "chrome", config.browser.userDataDir);
|
|
99
101
|
}
|
|
100
102
|
function mcpProviderForBrowserContextFactory(config, browserContextFactory) {
|
|
101
103
|
return {
|