@szymonrybczak/playwright-mcp 0.0.2 → 0.0.5
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/index.d.ts +20 -8
- package/lib/cjs/_virtual/_commonjsHelpers.js +9 -0
- package/lib/cjs/_virtual/browser.js +11 -0
- package/lib/cjs/_virtual/browser2.js +7 -0
- package/lib/cjs/cloudflare/package.json.js +10 -0
- package/lib/cjs/index.js +27 -0
- package/lib/cjs/node_modules/debug/src/browser.js +290 -0
- package/lib/cjs/node_modules/debug/src/common.js +307 -0
- package/lib/cjs/node_modules/ms/index.js +176 -0
- package/lib/cjs/package.js +9 -0
- package/lib/cjs/src/browserContextFactory.js +240 -0
- package/lib/cjs/src/config.js +77 -0
- package/lib/cjs/src/connection.js +74 -0
- package/lib/cjs/src/context.js +281 -0
- package/lib/cjs/src/fileUtils.js +25 -0
- package/lib/cjs/src/index.js +15 -0
- package/lib/cjs/src/javascript.js +41 -0
- package/lib/cjs/src/manualPromise.js +39 -0
- package/lib/cjs/src/pageSnapshot.js +35 -0
- package/lib/cjs/src/tab.js +91 -0
- package/lib/cjs/src/tools/common.js +60 -0
- package/lib/cjs/src/tools/console.js +36 -0
- package/lib/cjs/src/tools/dialogs.js +44 -0
- package/lib/cjs/src/tools/files.js +43 -0
- package/lib/cjs/src/tools/install.js +49 -0
- package/lib/cjs/src/tools/keyboard.js +38 -0
- package/lib/cjs/src/tools/navigate.js +85 -0
- package/lib/cjs/src/tools/network.js +43 -0
- package/lib/cjs/src/tools/pdf.js +42 -0
- package/lib/cjs/src/tools/screenshot.js +69 -0
- package/lib/cjs/src/tools/snapshot.js +195 -0
- package/lib/cjs/src/tools/tabs.js +110 -0
- package/lib/cjs/src/tools/testing.js +52 -0
- package/lib/cjs/src/tools/tool.js +9 -0
- package/lib/cjs/src/tools/utils.js +75 -0
- package/lib/cjs/src/tools/vision.js +181 -0
- package/lib/cjs/src/tools/wait.js +51 -0
- package/lib/cjs/src/tools.js +54 -0
- package/lib/esm/_virtual/_commonjsHelpers.js +5 -0
- package/lib/esm/_virtual/browser.js +7 -0
- package/lib/esm/_virtual/browser2.js +3 -0
- package/lib/esm/cloudflare/package.json.js +5 -0
- package/lib/esm/index.js +23 -0
- package/lib/esm/node_modules/debug/src/browser.js +286 -0
- package/lib/esm/node_modules/debug/src/common.js +303 -0
- package/lib/esm/node_modules/ms/index.js +172 -0
- package/lib/esm/package.js +5 -0
- package/lib/esm/src/browserContextFactory.js +216 -0
- package/lib/esm/src/config.js +72 -0
- package/lib/esm/src/connection.js +69 -0
- package/lib/esm/src/context.js +277 -0
- package/lib/esm/src/fileUtils.js +20 -0
- package/lib/esm/src/index.js +11 -0
- package/lib/esm/src/javascript.js +35 -0
- package/lib/esm/src/manualPromise.js +35 -0
- package/lib/esm/src/pageSnapshot.js +31 -0
- package/lib/esm/src/tab.js +87 -0
- package/lib/esm/src/tools/common.js +56 -0
- package/lib/esm/src/tools/console.js +32 -0
- package/lib/esm/src/tools/dialogs.js +40 -0
- package/lib/esm/src/tools/files.js +39 -0
- package/lib/esm/src/tools/install.js +45 -0
- package/lib/esm/src/tools/keyboard.js +34 -0
- package/lib/esm/src/tools/navigate.js +81 -0
- package/lib/esm/src/tools/network.js +39 -0
- package/lib/esm/src/tools/pdf.js +38 -0
- package/lib/esm/src/tools/screenshot.js +65 -0
- package/lib/esm/src/tools/snapshot.js +191 -0
- package/lib/esm/src/tools/tabs.js +106 -0
- package/lib/esm/src/tools/testing.js +48 -0
- package/lib/esm/src/tools/tool.js +5 -0
- package/lib/esm/src/tools/utils.js +68 -0
- package/lib/esm/src/tools/vision.js +177 -0
- package/lib/esm/src/tools/wait.js +47 -0
- package/lib/esm/src/tools.js +49 -0
- package/package.json +15 -44
- package/LICENSE +0 -202
- package/README.md +0 -508
- package/cli.js +0 -18
- package/config.d.ts +0 -128
- package/index.js +0 -19
- package/lib/browserContextFactory.js +0 -227
- package/lib/browserServer.js +0 -151
- package/lib/config.js +0 -189
- package/lib/connection.js +0 -82
- package/lib/context.js +0 -291
- package/lib/fileUtils.js +0 -32
- package/lib/httpServer.js +0 -201
- package/lib/index.js +0 -36
- package/lib/javascript.js +0 -49
- package/lib/manualPromise.js +0 -111
- package/lib/package.js +0 -20
- package/lib/pageSnapshot.js +0 -43
- package/lib/program.js +0 -72
- package/lib/server.js +0 -48
- package/lib/tab.js +0 -101
- package/lib/tools/common.js +0 -68
- package/lib/tools/console.js +0 -44
- package/lib/tools/dialogs.js +0 -52
- package/lib/tools/files.js +0 -51
- package/lib/tools/install.js +0 -57
- package/lib/tools/keyboard.js +0 -46
- package/lib/tools/navigate.js +0 -93
- package/lib/tools/network.js +0 -51
- package/lib/tools/pdf.js +0 -49
- package/lib/tools/screenshot.js +0 -77
- package/lib/tools/snapshot.js +0 -204
- package/lib/tools/tabs.js +0 -118
- package/lib/tools/testing.js +0 -60
- package/lib/tools/tool.js +0 -18
- package/lib/tools/utils.js +0 -80
- package/lib/tools/vision.js +0 -189
- package/lib/tools/wait.js +0 -59
- package/lib/tools.js +0 -61
- package/lib/transport.js +0 -133
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import debug from '../_virtual/browser.js';
|
|
2
|
+
import { waitForCompletion, callOnPageNoTrace } from './tools/utils.js';
|
|
3
|
+
import { ManualPromise } from './manualPromise.js';
|
|
4
|
+
import { Tab } from './tab.js';
|
|
5
|
+
import { outputFile } from './config.js';
|
|
6
|
+
|
|
7
|
+
const testDebug = debug("pw:mcp:test");
|
|
8
|
+
class Context {
|
|
9
|
+
tools;
|
|
10
|
+
config;
|
|
11
|
+
_browserContextPromise;
|
|
12
|
+
_browserContextFactory;
|
|
13
|
+
_tabs = [];
|
|
14
|
+
_currentTab;
|
|
15
|
+
_modalStates = [];
|
|
16
|
+
_pendingAction;
|
|
17
|
+
_downloads = [];
|
|
18
|
+
clientVersion;
|
|
19
|
+
constructor(tools, config, browserContextFactory) {
|
|
20
|
+
this.tools = tools;
|
|
21
|
+
this.config = config;
|
|
22
|
+
this._browserContextFactory = browserContextFactory;
|
|
23
|
+
testDebug("create context");
|
|
24
|
+
}
|
|
25
|
+
clientSupportsImages() {
|
|
26
|
+
if (this.config.imageResponses === "allow")
|
|
27
|
+
return true;
|
|
28
|
+
if (this.config.imageResponses === "omit")
|
|
29
|
+
return false;
|
|
30
|
+
return !this.clientVersion?.name.includes("cursor");
|
|
31
|
+
}
|
|
32
|
+
modalStates() {
|
|
33
|
+
return this._modalStates;
|
|
34
|
+
}
|
|
35
|
+
setModalState(modalState, inTab) {
|
|
36
|
+
this._modalStates.push({ ...modalState, tab: inTab });
|
|
37
|
+
}
|
|
38
|
+
clearModalState(modalState) {
|
|
39
|
+
this._modalStates = this._modalStates.filter((state) => state !== modalState);
|
|
40
|
+
}
|
|
41
|
+
modalStatesMarkdown() {
|
|
42
|
+
const result = ["### Modal state"];
|
|
43
|
+
if (this._modalStates.length === 0)
|
|
44
|
+
result.push("- There is no modal state present");
|
|
45
|
+
for (const state of this._modalStates) {
|
|
46
|
+
const tool = this.tools.find((tool2) => tool2.clearsModalState === state.type);
|
|
47
|
+
result.push(`- [${state.description}]: can be handled by the "${tool?.schema.name}" tool`);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
tabs() {
|
|
52
|
+
return this._tabs;
|
|
53
|
+
}
|
|
54
|
+
currentTabOrDie() {
|
|
55
|
+
if (!this._currentTab)
|
|
56
|
+
throw new Error("No current snapshot available. Capture a snapshot or navigate to a new location first.");
|
|
57
|
+
return this._currentTab;
|
|
58
|
+
}
|
|
59
|
+
async newTab() {
|
|
60
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
61
|
+
const page = await browserContext.newPage();
|
|
62
|
+
this._currentTab = this._tabs.find((t) => t.page === page);
|
|
63
|
+
return this._currentTab;
|
|
64
|
+
}
|
|
65
|
+
async selectTab(index) {
|
|
66
|
+
this._currentTab = this._tabs[index - 1];
|
|
67
|
+
await this._currentTab.page.bringToFront();
|
|
68
|
+
}
|
|
69
|
+
async ensureTab() {
|
|
70
|
+
const { browserContext } = await this._ensureBrowserContext();
|
|
71
|
+
if (!this._currentTab)
|
|
72
|
+
await browserContext.newPage();
|
|
73
|
+
return this._currentTab;
|
|
74
|
+
}
|
|
75
|
+
async listTabsMarkdown() {
|
|
76
|
+
if (!this._tabs.length)
|
|
77
|
+
return "### No tabs open";
|
|
78
|
+
const lines = ["### Open tabs"];
|
|
79
|
+
for (let i = 0; i < this._tabs.length; i++) {
|
|
80
|
+
const tab = this._tabs[i];
|
|
81
|
+
const title = await tab.title();
|
|
82
|
+
const url = tab.page.url();
|
|
83
|
+
const current = tab === this._currentTab ? " (current)" : "";
|
|
84
|
+
lines.push(`- ${i + 1}:${current} [${title}] (${url})`);
|
|
85
|
+
}
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
async closeTab(index) {
|
|
89
|
+
const tab = index === void 0 ? this._currentTab : this._tabs[index - 1];
|
|
90
|
+
await tab?.page.close();
|
|
91
|
+
return await this.listTabsMarkdown();
|
|
92
|
+
}
|
|
93
|
+
async run(tool, params) {
|
|
94
|
+
const toolResult = await tool.handle(this, tool.schema.inputSchema.parse(params || {}));
|
|
95
|
+
const { code, action, waitForNetwork, captureSnapshot, resultOverride } = toolResult;
|
|
96
|
+
const racingAction = action ? () => this._raceAgainstModalDialogs(action) : void 0;
|
|
97
|
+
if (resultOverride)
|
|
98
|
+
return resultOverride;
|
|
99
|
+
if (!this._currentTab) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text: 'No open pages available. Use the "browser_navigate" tool to navigate to a page first.'
|
|
104
|
+
}]
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const tab = this.currentTabOrDie();
|
|
108
|
+
let actionResult;
|
|
109
|
+
try {
|
|
110
|
+
if (waitForNetwork)
|
|
111
|
+
actionResult = await waitForCompletion(this, tab, async () => racingAction?.()) ?? void 0;
|
|
112
|
+
else
|
|
113
|
+
actionResult = await racingAction?.() ?? void 0;
|
|
114
|
+
} finally {
|
|
115
|
+
if (captureSnapshot && !this._javaScriptBlocked())
|
|
116
|
+
await tab.captureSnapshot();
|
|
117
|
+
}
|
|
118
|
+
const result = [];
|
|
119
|
+
result.push(`- Ran Playwright code:
|
|
120
|
+
\`\`\`js
|
|
121
|
+
${code.join("\n")}
|
|
122
|
+
\`\`\`
|
|
123
|
+
`);
|
|
124
|
+
if (this.modalStates().length) {
|
|
125
|
+
result.push(...this.modalStatesMarkdown());
|
|
126
|
+
return {
|
|
127
|
+
content: [{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: result.join("\n")
|
|
130
|
+
}]
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (this._downloads.length) {
|
|
134
|
+
result.push("", "### Downloads");
|
|
135
|
+
for (const entry of this._downloads) {
|
|
136
|
+
if (entry.finished)
|
|
137
|
+
result.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
|
|
138
|
+
else
|
|
139
|
+
result.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
|
|
140
|
+
}
|
|
141
|
+
result.push("");
|
|
142
|
+
}
|
|
143
|
+
if (this.tabs().length > 1)
|
|
144
|
+
result.push(await this.listTabsMarkdown(), "");
|
|
145
|
+
if (this.tabs().length > 1)
|
|
146
|
+
result.push("### Current tab");
|
|
147
|
+
result.push(
|
|
148
|
+
`- Page URL: ${tab.page.url()}`,
|
|
149
|
+
`- Page Title: ${await tab.title()}`
|
|
150
|
+
);
|
|
151
|
+
if (captureSnapshot && tab.hasSnapshot())
|
|
152
|
+
result.push(tab.snapshotOrDie().text());
|
|
153
|
+
const content = actionResult?.content ?? [];
|
|
154
|
+
return {
|
|
155
|
+
content: [
|
|
156
|
+
...content,
|
|
157
|
+
{
|
|
158
|
+
type: "text",
|
|
159
|
+
text: result.join("\n")
|
|
160
|
+
}
|
|
161
|
+
]
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async waitForTimeout(time) {
|
|
165
|
+
if (!this._currentTab || this._javaScriptBlocked()) {
|
|
166
|
+
await new Promise((f) => setTimeout(f, time));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
await callOnPageNoTrace(this._currentTab.page, (page) => {
|
|
170
|
+
return page.evaluate(() => new Promise((f) => setTimeout(f, 1e3)));
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async _raceAgainstModalDialogs(action) {
|
|
174
|
+
this._pendingAction = {
|
|
175
|
+
dialogShown: new ManualPromise()
|
|
176
|
+
};
|
|
177
|
+
let result;
|
|
178
|
+
try {
|
|
179
|
+
await Promise.race([
|
|
180
|
+
action().then((r) => result = r),
|
|
181
|
+
this._pendingAction.dialogShown
|
|
182
|
+
]);
|
|
183
|
+
} finally {
|
|
184
|
+
this._pendingAction = void 0;
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|
|
188
|
+
_javaScriptBlocked() {
|
|
189
|
+
return this._modalStates.some((state) => state.type === "dialog");
|
|
190
|
+
}
|
|
191
|
+
dialogShown(tab, dialog) {
|
|
192
|
+
this.setModalState({
|
|
193
|
+
type: "dialog",
|
|
194
|
+
description: `"${dialog.type()}" dialog with message "${dialog.message()}"`,
|
|
195
|
+
dialog
|
|
196
|
+
}, tab);
|
|
197
|
+
this._pendingAction?.dialogShown.resolve();
|
|
198
|
+
}
|
|
199
|
+
async downloadStarted(tab, download) {
|
|
200
|
+
const entry = {
|
|
201
|
+
download,
|
|
202
|
+
finished: false,
|
|
203
|
+
outputFile: await outputFile(this.config, download.suggestedFilename())
|
|
204
|
+
};
|
|
205
|
+
this._downloads.push(entry);
|
|
206
|
+
await download.saveAs(entry.outputFile);
|
|
207
|
+
entry.finished = true;
|
|
208
|
+
}
|
|
209
|
+
_onPageCreated(page) {
|
|
210
|
+
const tab = new Tab(this, page, (tab2) => this._onPageClosed(tab2));
|
|
211
|
+
this._tabs.push(tab);
|
|
212
|
+
if (!this._currentTab)
|
|
213
|
+
this._currentTab = tab;
|
|
214
|
+
}
|
|
215
|
+
_onPageClosed(tab) {
|
|
216
|
+
this._modalStates = this._modalStates.filter((state) => state.tab !== tab);
|
|
217
|
+
const index = this._tabs.indexOf(tab);
|
|
218
|
+
if (index === -1)
|
|
219
|
+
return;
|
|
220
|
+
this._tabs.splice(index, 1);
|
|
221
|
+
if (this._currentTab === tab)
|
|
222
|
+
this._currentTab = this._tabs[Math.min(index, this._tabs.length - 1)];
|
|
223
|
+
if (!this._tabs.length)
|
|
224
|
+
void this.close();
|
|
225
|
+
}
|
|
226
|
+
async close() {
|
|
227
|
+
if (!this._browserContextPromise)
|
|
228
|
+
return;
|
|
229
|
+
testDebug("close context");
|
|
230
|
+
const promise = this._browserContextPromise;
|
|
231
|
+
this._browserContextPromise = void 0;
|
|
232
|
+
await promise.then(async ({ browserContext, close }) => {
|
|
233
|
+
if (this.config.saveTrace)
|
|
234
|
+
await browserContext.tracing.stop();
|
|
235
|
+
await close();
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
async _setupRequestInterception(context) {
|
|
239
|
+
if (this.config.network?.allowedOrigins?.length) {
|
|
240
|
+
await context.route("**", (route) => route.abort("blockedbyclient"));
|
|
241
|
+
for (const origin of this.config.network.allowedOrigins)
|
|
242
|
+
await context.route(`*://${origin}/**`, (route) => route.continue());
|
|
243
|
+
}
|
|
244
|
+
if (this.config.network?.blockedOrigins?.length) {
|
|
245
|
+
for (const origin of this.config.network.blockedOrigins)
|
|
246
|
+
await context.route(`*://${origin}/**`, (route) => route.abort("blockedbyclient"));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
_ensureBrowserContext() {
|
|
250
|
+
if (!this._browserContextPromise) {
|
|
251
|
+
this._browserContextPromise = this._setupBrowserContext();
|
|
252
|
+
this._browserContextPromise.catch(() => {
|
|
253
|
+
this._browserContextPromise = void 0;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
return this._browserContextPromise;
|
|
257
|
+
}
|
|
258
|
+
async _setupBrowserContext() {
|
|
259
|
+
const result = await this._browserContextFactory.createContext();
|
|
260
|
+
const { browserContext } = result;
|
|
261
|
+
await this._setupRequestInterception(browserContext);
|
|
262
|
+
for (const page of browserContext.pages())
|
|
263
|
+
this._onPageCreated(page);
|
|
264
|
+
browserContext.on("page", (page) => this._onPageCreated(page));
|
|
265
|
+
if (this.config.saveTrace) {
|
|
266
|
+
await browserContext.tracing.start({
|
|
267
|
+
name: "trace",
|
|
268
|
+
screenshots: false,
|
|
269
|
+
snapshots: true,
|
|
270
|
+
sources: false
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export { Context };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
function cacheDir() {
|
|
5
|
+
let cacheDirectory;
|
|
6
|
+
if (process.platform === "linux")
|
|
7
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
|
|
8
|
+
else if (process.platform === "darwin")
|
|
9
|
+
cacheDirectory = path.join(os.homedir(), "Library", "Caches");
|
|
10
|
+
else if (process.platform === "win32")
|
|
11
|
+
cacheDirectory = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
|
12
|
+
else
|
|
13
|
+
throw new Error("Unsupported platform: " + process.platform);
|
|
14
|
+
return path.join(cacheDirectory, "ms-playwright");
|
|
15
|
+
}
|
|
16
|
+
async function userDataDir(browserConfig) {
|
|
17
|
+
return path.join(cacheDir(), "ms-playwright", `mcp-${browserConfig.launchOptions?.channel ?? browserConfig?.browserName}-profile`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { cacheDir, userDataDir };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createConnection as createConnection$1 } from './connection.js';
|
|
2
|
+
import { resolveConfig } from './config.js';
|
|
3
|
+
import { contextFactory } from './browserContextFactory.js';
|
|
4
|
+
|
|
5
|
+
async function createConnection(userConfig = {}, contextGetter) {
|
|
6
|
+
const config = await resolveConfig(userConfig);
|
|
7
|
+
const factory = contextFactory(config.browser);
|
|
8
|
+
return createConnection$1(config, factory);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { createConnection };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
function escapeWithQuotes(text, char = "'") {
|
|
2
|
+
const stringified = JSON.stringify(text);
|
|
3
|
+
const escapedText = stringified.substring(1, stringified.length - 1).replace(/\\"/g, '"');
|
|
4
|
+
if (char === "'")
|
|
5
|
+
return char + escapedText.replace(/[']/g, "\\'") + char;
|
|
6
|
+
if (char === '"')
|
|
7
|
+
return char + escapedText.replace(/["]/g, '\\"') + char;
|
|
8
|
+
if (char === "`")
|
|
9
|
+
return char + escapedText.replace(/[`]/g, "`") + char;
|
|
10
|
+
throw new Error("Invalid escape char");
|
|
11
|
+
}
|
|
12
|
+
function quote(text) {
|
|
13
|
+
return escapeWithQuotes(text, "'");
|
|
14
|
+
}
|
|
15
|
+
function formatObject(value, indent = " ") {
|
|
16
|
+
if (typeof value === "string")
|
|
17
|
+
return quote(value);
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return `[${value.map((o) => formatObject(o)).join(", ")}]`;
|
|
20
|
+
if (typeof value === "object") {
|
|
21
|
+
const keys = Object.keys(value).filter((key) => value[key] !== void 0).sort();
|
|
22
|
+
if (!keys.length)
|
|
23
|
+
return "{}";
|
|
24
|
+
const tokens = [];
|
|
25
|
+
for (const key of keys)
|
|
26
|
+
tokens.push(`${key}: ${formatObject(value[key])}`);
|
|
27
|
+
return `{
|
|
28
|
+
${indent}${tokens.join(`,
|
|
29
|
+
${indent}`)}
|
|
30
|
+
}`;
|
|
31
|
+
}
|
|
32
|
+
return String(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { escapeWithQuotes, formatObject, quote };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class ManualPromise extends Promise {
|
|
2
|
+
_resolve;
|
|
3
|
+
_reject;
|
|
4
|
+
_isDone;
|
|
5
|
+
constructor() {
|
|
6
|
+
let resolve;
|
|
7
|
+
let reject;
|
|
8
|
+
super((f, r) => {
|
|
9
|
+
resolve = f;
|
|
10
|
+
reject = r;
|
|
11
|
+
});
|
|
12
|
+
this._isDone = false;
|
|
13
|
+
this._resolve = resolve;
|
|
14
|
+
this._reject = reject;
|
|
15
|
+
}
|
|
16
|
+
isDone() {
|
|
17
|
+
return this._isDone;
|
|
18
|
+
}
|
|
19
|
+
resolve(t) {
|
|
20
|
+
this._isDone = true;
|
|
21
|
+
this._resolve(t);
|
|
22
|
+
}
|
|
23
|
+
reject(e) {
|
|
24
|
+
this._isDone = true;
|
|
25
|
+
this._reject(e);
|
|
26
|
+
}
|
|
27
|
+
static get [Symbol.species]() {
|
|
28
|
+
return Promise;
|
|
29
|
+
}
|
|
30
|
+
get [Symbol.toStringTag]() {
|
|
31
|
+
return "ManualPromise";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export { ManualPromise };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { callOnPageNoTrace } from './tools/utils.js';
|
|
2
|
+
|
|
3
|
+
class PageSnapshot {
|
|
4
|
+
_page;
|
|
5
|
+
_text;
|
|
6
|
+
constructor(page) {
|
|
7
|
+
this._page = page;
|
|
8
|
+
}
|
|
9
|
+
static async create(page) {
|
|
10
|
+
const snapshot = new PageSnapshot(page);
|
|
11
|
+
await snapshot._build();
|
|
12
|
+
return snapshot;
|
|
13
|
+
}
|
|
14
|
+
text() {
|
|
15
|
+
return this._text;
|
|
16
|
+
}
|
|
17
|
+
async _build() {
|
|
18
|
+
const snapshot = await callOnPageNoTrace(this._page, (page) => page._snapshotForAI());
|
|
19
|
+
this._text = [
|
|
20
|
+
`- Page Snapshot`,
|
|
21
|
+
"```yaml",
|
|
22
|
+
snapshot,
|
|
23
|
+
"```"
|
|
24
|
+
].join("\n");
|
|
25
|
+
}
|
|
26
|
+
refLocator(params) {
|
|
27
|
+
return this._page.locator(`aria-ref=${params.ref}`).describe(params.element);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { PageSnapshot };
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { PageSnapshot } from './pageSnapshot.js';
|
|
2
|
+
import { callOnPageNoTrace } from './tools/utils.js';
|
|
3
|
+
|
|
4
|
+
class Tab {
|
|
5
|
+
context;
|
|
6
|
+
page;
|
|
7
|
+
_consoleMessages = [];
|
|
8
|
+
_requests = /* @__PURE__ */ new Map();
|
|
9
|
+
_snapshot;
|
|
10
|
+
_onPageClose;
|
|
11
|
+
constructor(context, page, onPageClose) {
|
|
12
|
+
this.context = context;
|
|
13
|
+
this.page = page;
|
|
14
|
+
this._onPageClose = onPageClose;
|
|
15
|
+
page.on("console", (event) => this._consoleMessages.push(event));
|
|
16
|
+
page.on("request", (request) => this._requests.set(request, null));
|
|
17
|
+
page.on("response", (response) => this._requests.set(response.request(), response));
|
|
18
|
+
page.on("close", () => this._onClose());
|
|
19
|
+
page.on("filechooser", (chooser) => {
|
|
20
|
+
this.context.setModalState({
|
|
21
|
+
type: "fileChooser",
|
|
22
|
+
description: "File chooser",
|
|
23
|
+
fileChooser: chooser
|
|
24
|
+
}, this);
|
|
25
|
+
});
|
|
26
|
+
page.on("dialog", (dialog) => this.context.dialogShown(this, dialog));
|
|
27
|
+
page.on("download", (download) => {
|
|
28
|
+
void this.context.downloadStarted(this, download);
|
|
29
|
+
});
|
|
30
|
+
page.setDefaultNavigationTimeout(6e4);
|
|
31
|
+
page.setDefaultTimeout(5e3);
|
|
32
|
+
}
|
|
33
|
+
_clearCollectedArtifacts() {
|
|
34
|
+
this._consoleMessages.length = 0;
|
|
35
|
+
this._requests.clear();
|
|
36
|
+
}
|
|
37
|
+
_onClose() {
|
|
38
|
+
this._clearCollectedArtifacts();
|
|
39
|
+
this._onPageClose(this);
|
|
40
|
+
}
|
|
41
|
+
async title() {
|
|
42
|
+
return await callOnPageNoTrace(this.page, (page) => page.title());
|
|
43
|
+
}
|
|
44
|
+
async waitForLoadState(state, options) {
|
|
45
|
+
await callOnPageNoTrace(this.page, (page) => page.waitForLoadState(state, options).catch(() => {
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
async navigate(url) {
|
|
49
|
+
this._clearCollectedArtifacts();
|
|
50
|
+
const downloadEvent = callOnPageNoTrace(this.page, (page) => page.waitForEvent("download").catch(() => {
|
|
51
|
+
}));
|
|
52
|
+
try {
|
|
53
|
+
await this.page.goto(url, { waitUntil: "domcontentloaded" });
|
|
54
|
+
} catch (_e) {
|
|
55
|
+
const e = _e;
|
|
56
|
+
const mightBeDownload = e.message.includes("net::ERR_ABORTED") || e.message.includes("Download is starting");
|
|
57
|
+
if (!mightBeDownload)
|
|
58
|
+
throw e;
|
|
59
|
+
const download = await Promise.race([
|
|
60
|
+
downloadEvent,
|
|
61
|
+
new Promise((resolve) => setTimeout(resolve, 1e3))
|
|
62
|
+
]);
|
|
63
|
+
if (!download)
|
|
64
|
+
throw e;
|
|
65
|
+
}
|
|
66
|
+
await this.waitForLoadState("load", { timeout: 5e3 });
|
|
67
|
+
}
|
|
68
|
+
hasSnapshot() {
|
|
69
|
+
return !!this._snapshot;
|
|
70
|
+
}
|
|
71
|
+
snapshotOrDie() {
|
|
72
|
+
if (!this._snapshot)
|
|
73
|
+
throw new Error("No snapshot available");
|
|
74
|
+
return this._snapshot;
|
|
75
|
+
}
|
|
76
|
+
consoleMessages() {
|
|
77
|
+
return this._consoleMessages;
|
|
78
|
+
}
|
|
79
|
+
requests() {
|
|
80
|
+
return this._requests;
|
|
81
|
+
}
|
|
82
|
+
async captureSnapshot() {
|
|
83
|
+
this._snapshot = await PageSnapshot.create(this.page);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { Tab };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from './tool.js';
|
|
3
|
+
|
|
4
|
+
const close = defineTool({
|
|
5
|
+
capability: "core",
|
|
6
|
+
schema: {
|
|
7
|
+
name: "browser_close",
|
|
8
|
+
title: "Close browser",
|
|
9
|
+
description: "Close the page",
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
type: "readOnly"
|
|
12
|
+
},
|
|
13
|
+
handle: async (context) => {
|
|
14
|
+
await context.close();
|
|
15
|
+
return {
|
|
16
|
+
code: [`await page.close()`],
|
|
17
|
+
captureSnapshot: false,
|
|
18
|
+
waitForNetwork: false
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
const resize = (captureSnapshot) => defineTool({
|
|
23
|
+
capability: "core",
|
|
24
|
+
schema: {
|
|
25
|
+
name: "browser_resize",
|
|
26
|
+
title: "Resize browser window",
|
|
27
|
+
description: "Resize the browser window",
|
|
28
|
+
inputSchema: z.object({
|
|
29
|
+
width: z.coerce.number().describe("Width of the browser window"),
|
|
30
|
+
height: z.coerce.number().describe("Height of the browser window")
|
|
31
|
+
}),
|
|
32
|
+
type: "readOnly"
|
|
33
|
+
},
|
|
34
|
+
handle: async (context, params) => {
|
|
35
|
+
const tab = context.currentTabOrDie();
|
|
36
|
+
const code = [
|
|
37
|
+
`// Resize browser window to ${params.width}x${params.height}`,
|
|
38
|
+
`await page.setViewportSize({ width: ${params.width}, height: ${params.height} });`
|
|
39
|
+
];
|
|
40
|
+
const action = async () => {
|
|
41
|
+
await tab.page.setViewportSize({ width: params.width, height: params.height });
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
code,
|
|
45
|
+
action,
|
|
46
|
+
captureSnapshot,
|
|
47
|
+
waitForNetwork: true
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
const common = (captureSnapshot) => [
|
|
52
|
+
close,
|
|
53
|
+
resize(captureSnapshot)
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
export { common as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from './tool.js';
|
|
3
|
+
|
|
4
|
+
const console = defineTool({
|
|
5
|
+
capability: "core",
|
|
6
|
+
schema: {
|
|
7
|
+
name: "browser_console_messages",
|
|
8
|
+
title: "Get console messages",
|
|
9
|
+
description: "Returns all console messages",
|
|
10
|
+
inputSchema: z.object({}),
|
|
11
|
+
type: "readOnly"
|
|
12
|
+
},
|
|
13
|
+
handle: async (context) => {
|
|
14
|
+
const messages = context.currentTabOrDie().consoleMessages();
|
|
15
|
+
const log = messages.map((message) => `[${message.type().toUpperCase()}] ${message.text()}`).join("\n");
|
|
16
|
+
return {
|
|
17
|
+
code: [`// <internal code to get console messages>`],
|
|
18
|
+
action: async () => {
|
|
19
|
+
return {
|
|
20
|
+
content: [{ type: "text", text: log }]
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
captureSnapshot: false,
|
|
24
|
+
waitForNetwork: false
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
const console$1 = [
|
|
29
|
+
console
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export { console$1 as default };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from './tool.js';
|
|
3
|
+
|
|
4
|
+
const handleDialog = (captureSnapshot) => defineTool({
|
|
5
|
+
capability: "core",
|
|
6
|
+
schema: {
|
|
7
|
+
name: "browser_handle_dialog",
|
|
8
|
+
title: "Handle a dialog",
|
|
9
|
+
description: "Handle a dialog",
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
accept: z.coerce.boolean().describe("Whether to accept the dialog."),
|
|
12
|
+
promptText: z.string().optional().describe("The text of the prompt in case of a prompt dialog.")
|
|
13
|
+
}),
|
|
14
|
+
type: "destructive"
|
|
15
|
+
},
|
|
16
|
+
handle: async (context, params) => {
|
|
17
|
+
const dialogState = context.modalStates().find((state) => state.type === "dialog");
|
|
18
|
+
if (!dialogState)
|
|
19
|
+
throw new Error("No dialog visible");
|
|
20
|
+
if (params.accept)
|
|
21
|
+
await dialogState.dialog.accept(params.promptText);
|
|
22
|
+
else
|
|
23
|
+
await dialogState.dialog.dismiss();
|
|
24
|
+
context.clearModalState(dialogState);
|
|
25
|
+
const code = [
|
|
26
|
+
`// <internal code to handle "${dialogState.dialog.type()}" dialog>`
|
|
27
|
+
];
|
|
28
|
+
return {
|
|
29
|
+
code,
|
|
30
|
+
captureSnapshot,
|
|
31
|
+
waitForNetwork: false
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
clearsModalState: "dialog"
|
|
35
|
+
});
|
|
36
|
+
const dialogs = (captureSnapshot) => [
|
|
37
|
+
handleDialog(captureSnapshot)
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
export { dialogs as default };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from './tool.js';
|
|
3
|
+
|
|
4
|
+
const uploadFile = (captureSnapshot) => defineTool({
|
|
5
|
+
capability: "files",
|
|
6
|
+
schema: {
|
|
7
|
+
name: "browser_file_upload",
|
|
8
|
+
title: "Upload files",
|
|
9
|
+
description: "Upload one or multiple files",
|
|
10
|
+
inputSchema: z.object({
|
|
11
|
+
paths: z.array(z.string()).describe("The absolute paths to the files to upload. Can be a single file or multiple files.")
|
|
12
|
+
}),
|
|
13
|
+
type: "destructive"
|
|
14
|
+
},
|
|
15
|
+
handle: async (context, params) => {
|
|
16
|
+
const modalState = context.modalStates().find((state) => state.type === "fileChooser");
|
|
17
|
+
if (!modalState)
|
|
18
|
+
throw new Error("No file chooser visible");
|
|
19
|
+
const code = [
|
|
20
|
+
`// <internal code to chose files ${params.paths.join(", ")}`
|
|
21
|
+
];
|
|
22
|
+
const action = async () => {
|
|
23
|
+
await modalState.fileChooser.setFiles(params.paths);
|
|
24
|
+
context.clearModalState(modalState);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
code,
|
|
28
|
+
action,
|
|
29
|
+
captureSnapshot,
|
|
30
|
+
waitForNetwork: true
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
clearsModalState: "fileChooser"
|
|
34
|
+
});
|
|
35
|
+
const files = (captureSnapshot) => [
|
|
36
|
+
uploadFile(captureSnapshot)
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export { files as default };
|