@wong2kim/wmux 1.1.0 → 1.1.2
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 +14 -4
- package/dist/cli/cli/commands/browser.js +101 -77
- package/dist/cli/cli/index.js +6 -6
- package/dist/cli/shared/constants.js +3 -0
- package/dist/cli/shared/rpc.js +15 -4
- package/dist/mcp/mcp/index.js +41 -21
- package/dist/mcp/mcp/playwright/PlaywrightEngine.js +186 -0
- package/dist/mcp/mcp/playwright/anti-detection.js +58 -0
- package/dist/mcp/mcp/playwright/dom-intelligence.js +171 -0
- package/dist/mcp/mcp/playwright/human-typing.js +48 -0
- package/dist/mcp/mcp/playwright/markdown-extractor.js +520 -0
- package/dist/mcp/mcp/playwright/snapshot.js +261 -0
- package/dist/mcp/mcp/playwright/tools/extraction.js +143 -0
- package/dist/mcp/mcp/playwright/tools/file.js +274 -0
- package/dist/mcp/mcp/playwright/tools/inspection.js +395 -0
- package/dist/mcp/mcp/playwright/tools/interaction.js +387 -0
- package/dist/mcp/mcp/playwright/tools/navigation.js +183 -0
- package/dist/mcp/mcp/playwright/tools/state.js +410 -0
- package/dist/mcp/mcp/playwright/tools/utility.js +167 -0
- package/dist/mcp/mcp/playwright/tools/wait.js +111 -0
- package/dist/mcp/shared/constants.js +3 -0
- package/dist/mcp/shared/rpc.js +15 -4
- package/package.json +7 -4
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyAntiDetection = applyAntiDetection;
|
|
4
|
+
exports.evaluateWithGesture = evaluateWithGesture;
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Anti-detection helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/**
|
|
9
|
+
* Apply anti-detection measures to the page via init scripts.
|
|
10
|
+
*
|
|
11
|
+
* Currently patches `navigator.webdriver` to return `undefined` so that
|
|
12
|
+
* common bot-detection scripts do not flag the session.
|
|
13
|
+
*/
|
|
14
|
+
async function applyAntiDetection(page) {
|
|
15
|
+
await page.context().addInitScript(() => {
|
|
16
|
+
// Override navigator.webdriver to hide automation flag
|
|
17
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
18
|
+
get: () => undefined,
|
|
19
|
+
configurable: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// CDP-powered evaluate with user gesture
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/**
|
|
27
|
+
* Evaluate a JavaScript expression in the page context with the
|
|
28
|
+
* `userGesture` flag set to `true`.
|
|
29
|
+
*
|
|
30
|
+
* This is useful for actions that require a transient user activation
|
|
31
|
+
* (e.g. opening a popup, triggering downloads) without an actual mouse /
|
|
32
|
+
* keyboard event.
|
|
33
|
+
*
|
|
34
|
+
* Internally opens a CDP session and calls `Runtime.evaluate`.
|
|
35
|
+
*/
|
|
36
|
+
async function evaluateWithGesture(page, expression) {
|
|
37
|
+
const client = await page.context().newCDPSession(page);
|
|
38
|
+
try {
|
|
39
|
+
const result = await client.send('Runtime.evaluate', {
|
|
40
|
+
expression,
|
|
41
|
+
userGesture: true,
|
|
42
|
+
returnByValue: true,
|
|
43
|
+
awaitPromise: true,
|
|
44
|
+
});
|
|
45
|
+
if (result.exceptionDetails) {
|
|
46
|
+
const msg = result.exceptionDetails.exception?.description ??
|
|
47
|
+
result.exceptionDetails.text ??
|
|
48
|
+
'CDP Runtime.evaluate threw an exception';
|
|
49
|
+
throw new Error(msg);
|
|
50
|
+
}
|
|
51
|
+
return result.result.value;
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await client.detach().catch(() => {
|
|
55
|
+
/* best-effort cleanup */
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSmartSnapshot = getSmartSnapshot;
|
|
4
|
+
exports.getLocatorByRef = getLocatorByRef;
|
|
5
|
+
exports.clearElementCache = clearElementCache;
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Constants
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
const DEFAULT_MAX_CONTENT_LENGTH = 3000;
|
|
10
|
+
/** Roles considered interactive — elements with these roles get indexed */
|
|
11
|
+
const INTERACTIVE_ROLES = new Set([
|
|
12
|
+
'button',
|
|
13
|
+
'link',
|
|
14
|
+
'textbox',
|
|
15
|
+
'checkbox',
|
|
16
|
+
'radio',
|
|
17
|
+
'combobox',
|
|
18
|
+
'listbox',
|
|
19
|
+
'menuitem',
|
|
20
|
+
'menuitemcheckbox',
|
|
21
|
+
'menuitemradio',
|
|
22
|
+
'option',
|
|
23
|
+
'searchbox',
|
|
24
|
+
'slider',
|
|
25
|
+
'spinbutton',
|
|
26
|
+
'switch',
|
|
27
|
+
'tab',
|
|
28
|
+
'treeitem',
|
|
29
|
+
]);
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Element cache — stores indexed elements from the last snapshot
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
let elementCache = [];
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
/**
|
|
38
|
+
* Escape special characters in a string for use inside a Playwright
|
|
39
|
+
* locator expression (e.g. `getByRole('button', { name: '...' })`).
|
|
40
|
+
*/
|
|
41
|
+
function escapeLocatorName(name) {
|
|
42
|
+
return name.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a Playwright locator string for a given role and name.
|
|
46
|
+
*
|
|
47
|
+
* If the name is empty, falls back to `getByRole('role')` without a
|
|
48
|
+
* name filter. When duplicate names exist for the same role, callers
|
|
49
|
+
* should use `.nth()` — but we provide the base locator here.
|
|
50
|
+
*/
|
|
51
|
+
function buildLocatorString(role, name) {
|
|
52
|
+
if (!name) {
|
|
53
|
+
return `getByRole('${role}')`;
|
|
54
|
+
}
|
|
55
|
+
return `getByRole('${role}', { name: '${escapeLocatorName(name)}' })`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Recursively walk the CDP accessibility tree and collect interactive
|
|
59
|
+
* elements into the provided array, assigning 1-based ref numbers.
|
|
60
|
+
*/
|
|
61
|
+
function collectInteractiveElements(nodeMap, node, elements) {
|
|
62
|
+
if (node.ignored)
|
|
63
|
+
return;
|
|
64
|
+
const role = node.role?.value ?? 'none';
|
|
65
|
+
const name = node.name?.value ?? '';
|
|
66
|
+
if (INTERACTIVE_ROLES.has(role)) {
|
|
67
|
+
const ref = elements.length + 1; // 1-based
|
|
68
|
+
const element = {
|
|
69
|
+
ref,
|
|
70
|
+
role,
|
|
71
|
+
name,
|
|
72
|
+
locator: buildLocatorString(role, name),
|
|
73
|
+
};
|
|
74
|
+
if (node.value?.value) {
|
|
75
|
+
element.value = node.value.value;
|
|
76
|
+
}
|
|
77
|
+
if (node.description?.value) {
|
|
78
|
+
element.description = node.description.value;
|
|
79
|
+
}
|
|
80
|
+
elements.push(element);
|
|
81
|
+
}
|
|
82
|
+
// Recurse into children
|
|
83
|
+
if (node.childIds) {
|
|
84
|
+
for (const childId of node.childIds) {
|
|
85
|
+
const child = nodeMap.get(childId);
|
|
86
|
+
if (child) {
|
|
87
|
+
collectInteractiveElements(nodeMap, child, elements);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Fetch the full accessibility tree via CDP and return indexed interactive
|
|
94
|
+
* elements.
|
|
95
|
+
*/
|
|
96
|
+
async function getInteractiveElements(page) {
|
|
97
|
+
const client = await page.context().newCDPSession(page);
|
|
98
|
+
try {
|
|
99
|
+
const { nodes } = (await client.send('Accessibility.getFullAXTree'));
|
|
100
|
+
if (nodes.length === 0)
|
|
101
|
+
return [];
|
|
102
|
+
// Build a map for quick lookup by nodeId
|
|
103
|
+
const nodeMap = new Map();
|
|
104
|
+
for (const n of nodes)
|
|
105
|
+
nodeMap.set(n.nodeId, n);
|
|
106
|
+
const elements = [];
|
|
107
|
+
collectInteractiveElements(nodeMap, nodes[0], elements);
|
|
108
|
+
return elements;
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await client.detach().catch(() => {
|
|
112
|
+
/* best-effort cleanup */
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Retrieve truncated page text content.
|
|
118
|
+
*/
|
|
119
|
+
async function getPageContent(page, maxLength) {
|
|
120
|
+
try {
|
|
121
|
+
const text = await page.innerText('body');
|
|
122
|
+
if (text.length <= maxLength)
|
|
123
|
+
return text;
|
|
124
|
+
return text.slice(0, maxLength) + '\n... (truncated)';
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Public API
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
/**
|
|
134
|
+
* Generate a "smart snapshot" of the page: a structured representation
|
|
135
|
+
* containing only interactive elements (with 1-based ref indices) plus a
|
|
136
|
+
* truncated text summary of the page content.
|
|
137
|
+
*
|
|
138
|
+
* The indexed elements are cached internally so that `getLocatorByRef()`
|
|
139
|
+
* can resolve a ref number back to a Playwright locator string without
|
|
140
|
+
* re-querying the page.
|
|
141
|
+
*/
|
|
142
|
+
async function getSmartSnapshot(page, options) {
|
|
143
|
+
const maxContentLength = options?.maxContentLength ?? DEFAULT_MAX_CONTENT_LENGTH;
|
|
144
|
+
const [url, title, elements, content] = await Promise.all([
|
|
145
|
+
Promise.resolve(page.url()),
|
|
146
|
+
page.title(),
|
|
147
|
+
getInteractiveElements(page),
|
|
148
|
+
getPageContent(page, maxContentLength),
|
|
149
|
+
]);
|
|
150
|
+
// Update element cache
|
|
151
|
+
elementCache = elements;
|
|
152
|
+
return { url, title, elements, content };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Look up a Playwright locator string by the 1-based ref number assigned
|
|
156
|
+
* during the most recent `getSmartSnapshot()` call.
|
|
157
|
+
*
|
|
158
|
+
* Returns `null` if the ref is out of range or no snapshot has been taken.
|
|
159
|
+
*/
|
|
160
|
+
function getLocatorByRef(ref) {
|
|
161
|
+
if (ref < 1 || ref > elementCache.length)
|
|
162
|
+
return null;
|
|
163
|
+
return elementCache[ref - 1].locator;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Clear the cached element list. Useful when navigating to a new page
|
|
167
|
+
* to avoid stale refs.
|
|
168
|
+
*/
|
|
169
|
+
function clearElementCache() {
|
|
170
|
+
elementCache = [];
|
|
171
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generateDelaySchedule = generateDelaySchedule;
|
|
4
|
+
exports.typeHumanlike = typeHumanlike;
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Internal helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const DEFAULT_MIN_DELAY = 50;
|
|
9
|
+
const DEFAULT_MAX_DELAY = 150;
|
|
10
|
+
function randomDelay(min, max) {
|
|
11
|
+
return Math.random() * (max - min) + min;
|
|
12
|
+
}
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
}
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Public API
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
/**
|
|
20
|
+
* Generate an array of per-character delay values (in ms) for the given
|
|
21
|
+
* text. This mirrors `HumanBehavior.generateTypingSchedule()` from the
|
|
22
|
+
* main process but is independent of that class.
|
|
23
|
+
*/
|
|
24
|
+
function generateDelaySchedule(text, options) {
|
|
25
|
+
const min = options?.minDelay ?? DEFAULT_MIN_DELAY;
|
|
26
|
+
const max = options?.maxDelay ?? DEFAULT_MAX_DELAY;
|
|
27
|
+
return Array.from({ length: text.length }, () => randomDelay(min, max));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type `text` into the element identified by `selector` with randomised
|
|
31
|
+
* inter-keystroke delays that mimic human typing.
|
|
32
|
+
*
|
|
33
|
+
* Each character is pressed individually via `page.keyboard.press()` with
|
|
34
|
+
* a random pause between `minDelay` and `maxDelay` milliseconds.
|
|
35
|
+
*
|
|
36
|
+
* If `selector` is provided the element is clicked first to ensure focus.
|
|
37
|
+
*/
|
|
38
|
+
async function typeHumanlike(page, selector, text, options) {
|
|
39
|
+
// Focus the target element
|
|
40
|
+
if (selector) {
|
|
41
|
+
await page.click(selector);
|
|
42
|
+
}
|
|
43
|
+
const delays = generateDelaySchedule(text, options);
|
|
44
|
+
for (let i = 0; i < text.length; i++) {
|
|
45
|
+
await page.keyboard.press(text[i]);
|
|
46
|
+
await sleep(delays[i]);
|
|
47
|
+
}
|
|
48
|
+
}
|