@marktoflow/integrations 2.0.0-alpha.12
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 +1341 -0
- package/dist/adapters/claude-agent-hooks.d.ts.map +1 -0
- package/dist/adapters/claude-agent-hooks.js +339 -0
- package/dist/adapters/claude-agent-hooks.js.map +1 -0
- package/dist/adapters/claude-agent-types.d.ts +426 -0
- package/dist/adapters/claude-agent-types.d.ts.map +1 -0
- package/dist/adapters/claude-agent-types.js +62 -0
- package/dist/adapters/claude-agent-types.js.map +1 -0
- package/dist/adapters/claude-agent-workflow.d.ts +393 -0
- package/dist/adapters/claude-agent-workflow.d.ts.map +1 -0
- package/dist/adapters/claude-agent-workflow.js +445 -0
- package/dist/adapters/claude-agent-workflow.js.map +1 -0
- package/dist/adapters/claude-agent.d.ts +189 -0
- package/dist/adapters/claude-agent.d.ts.map +1 -0
- package/dist/adapters/claude-agent.js +504 -0
- package/dist/adapters/claude-agent.js.map +1 -0
- package/dist/adapters/claude-code.d.ts +34 -0
- package/dist/adapters/claude-code.d.ts.map +1 -0
- package/dist/adapters/claude-code.js +89 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex-types.d.ts +463 -0
- package/dist/adapters/codex-types.d.ts.map +1 -0
- package/dist/adapters/codex-types.js +53 -0
- package/dist/adapters/codex-types.js.map +1 -0
- package/dist/adapters/codex-workflow.d.ts +414 -0
- package/dist/adapters/codex-workflow.d.ts.map +1 -0
- package/dist/adapters/codex-workflow.js +470 -0
- package/dist/adapters/codex-workflow.js.map +1 -0
- package/dist/adapters/codex.d.ts +154 -0
- package/dist/adapters/codex.d.ts.map +1 -0
- package/dist/adapters/codex.js +416 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/github-copilot-types.d.ts +1012 -0
- package/dist/adapters/github-copilot-types.d.ts.map +1 -0
- package/dist/adapters/github-copilot-types.js +80 -0
- package/dist/adapters/github-copilot-types.js.map +1 -0
- package/dist/adapters/github-copilot-workflow.d.ts +462 -0
- package/dist/adapters/github-copilot-workflow.d.ts.map +1 -0
- package/dist/adapters/github-copilot-workflow.js +473 -0
- package/dist/adapters/github-copilot-workflow.js.map +1 -0
- package/dist/adapters/github-copilot.d.ts +280 -0
- package/dist/adapters/github-copilot.d.ts.map +1 -0
- package/dist/adapters/github-copilot.js +468 -0
- package/dist/adapters/github-copilot.js.map +1 -0
- package/dist/adapters/ollama.d.ts.map +1 -0
- package/dist/adapters/ollama.js +9 -0
- package/dist/adapters/ollama.js.map +1 -0
- package/dist/adapters/opencode.d.ts +41 -0
- package/dist/adapters/opencode.d.ts.map +1 -0
- package/dist/adapters/opencode.js +148 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +161 -0
- package/dist/index.js.map +1 -0
- package/dist/services/ai-browser.d.ts +112 -0
- package/dist/services/ai-browser.d.ts.map +1 -0
- package/dist/services/ai-browser.js +363 -0
- package/dist/services/ai-browser.js.map +1 -0
- package/dist/services/airtable.d.ts.map +1 -0
- package/dist/services/airtable.js +295 -0
- package/dist/services/airtable.js.map +1 -0
- package/dist/services/asana.d.ts +159 -0
- package/dist/services/asana.d.ts.map +1 -0
- package/dist/services/asana.js +196 -0
- package/dist/services/asana.js.map +1 -0
- package/dist/services/aws-s3.d.ts +89 -0
- package/dist/services/aws-s3.d.ts.map +1 -0
- package/dist/services/aws-s3.js +154 -0
- package/dist/services/aws-s3.js.map +1 -0
- package/dist/services/confluence.d.ts.map +1 -0
- package/dist/services/confluence.js +356 -0
- package/dist/services/confluence.js.map +1 -0
- package/dist/services/discord.d.ts.map +1 -0
- package/dist/services/discord.js +279 -0
- package/dist/services/discord.js.map +1 -0
- package/dist/services/dropbox.d.ts +134 -0
- package/dist/services/dropbox.d.ts.map +1 -0
- package/dist/services/dropbox.js +190 -0
- package/dist/services/dropbox.js.map +1 -0
- package/dist/services/github.d.ts.map +1 -0
- package/dist/services/github.js +13 -0
- package/dist/services/github.js.map +1 -0
- package/dist/services/gmail-trigger.d.ts.map +1 -0
- package/dist/services/gmail-trigger.js +172 -0
- package/dist/services/gmail-trigger.js.map +1 -0
- package/dist/services/gmail.d.ts.map +1 -0
- package/dist/services/gmail.js +372 -0
- package/dist/services/gmail.js.map +1 -0
- package/dist/services/google-calendar.d.ts.map +1 -0
- package/dist/services/google-calendar.js +357 -0
- package/dist/services/google-calendar.js.map +1 -0
- package/dist/services/google-docs.d.ts.map +1 -0
- package/dist/services/google-docs.js +332 -0
- package/dist/services/google-docs.js.map +1 -0
- package/dist/services/google-drive.d.ts.map +1 -0
- package/dist/services/google-drive.js +327 -0
- package/dist/services/google-drive.js.map +1 -0
- package/dist/services/google-sheets.d.ts.map +1 -0
- package/dist/services/google-sheets.js +391 -0
- package/dist/services/google-sheets.js.map +1 -0
- package/dist/services/http.d.ts.map +1 -0
- package/dist/services/http.js +284 -0
- package/dist/services/http.js.map +1 -0
- package/dist/services/jira.d.ts.map +1 -0
- package/dist/services/jira.js +35 -0
- package/dist/services/jira.js.map +1 -0
- package/dist/services/linear.d.ts.map +1 -0
- package/dist/services/linear.js +326 -0
- package/dist/services/linear.js.map +1 -0
- package/dist/services/mailchimp.d.ts +169 -0
- package/dist/services/mailchimp.d.ts.map +1 -0
- package/dist/services/mailchimp.js +180 -0
- package/dist/services/mailchimp.js.map +1 -0
- package/dist/services/mysql.d.ts.map +1 -0
- package/dist/services/mysql.js +250 -0
- package/dist/services/mysql.js.map +1 -0
- package/dist/services/notion.d.ts.map +1 -0
- package/dist/services/notion.js +358 -0
- package/dist/services/notion.js.map +1 -0
- package/dist/services/outlook-trigger.d.ts.map +1 -0
- package/dist/services/outlook-trigger.js +204 -0
- package/dist/services/outlook-trigger.js.map +1 -0
- package/dist/services/outlook.d.ts.map +1 -0
- package/dist/services/outlook.js +486 -0
- package/dist/services/outlook.js.map +1 -0
- package/dist/services/playwright.d.ts +678 -0
- package/dist/services/playwright.d.ts.map +1 -0
- package/dist/services/playwright.js +1141 -0
- package/dist/services/playwright.js.map +1 -0
- package/dist/services/postgres.d.ts.map +1 -0
- package/dist/services/postgres.js +230 -0
- package/dist/services/postgres.js.map +1 -0
- package/dist/services/sendgrid.d.ts +43 -0
- package/dist/services/sendgrid.d.ts.map +1 -0
- package/dist/services/sendgrid.js +87 -0
- package/dist/services/sendgrid.js.map +1 -0
- package/dist/services/shopify.d.ts +160 -0
- package/dist/services/shopify.d.ts.map +1 -0
- package/dist/services/shopify.js +166 -0
- package/dist/services/shopify.js.map +1 -0
- package/dist/services/slack-socket.d.ts.map +1 -0
- package/dist/services/slack-socket.js +48 -0
- package/dist/services/slack-socket.js.map +1 -0
- package/dist/services/slack.d.ts.map +1 -0
- package/dist/services/slack.js +11 -0
- package/dist/services/slack.js.map +1 -0
- package/dist/services/stripe.d.ts +275 -0
- package/dist/services/stripe.d.ts.map +1 -0
- package/dist/services/stripe.js +229 -0
- package/dist/services/stripe.js.map +1 -0
- package/dist/services/supabase.d.ts.map +1 -0
- package/dist/services/supabase.js +328 -0
- package/dist/services/supabase.js.map +1 -0
- package/dist/services/teams.d.ts +224 -0
- package/dist/services/teams.d.ts.map +1 -0
- package/dist/services/teams.js +229 -0
- package/dist/services/teams.js.map +1 -0
- package/dist/services/telegram.d.ts.map +1 -0
- package/dist/services/telegram.js +247 -0
- package/dist/services/telegram.js.map +1 -0
- package/dist/services/trello.d.ts +160 -0
- package/dist/services/trello.d.ts.map +1 -0
- package/dist/services/trello.js +194 -0
- package/dist/services/trello.js.map +1 -0
- package/dist/services/twilio.d.ts +126 -0
- package/dist/services/twilio.d.ts.map +1 -0
- package/dist/services/twilio.js +153 -0
- package/dist/services/twilio.js.map +1 -0
- package/dist/services/whatsapp.d.ts.map +1 -0
- package/dist/services/whatsapp.js +253 -0
- package/dist/services/whatsapp.js.map +1 -0
- package/dist/services/zendesk.d.ts +134 -0
- package/dist/services/zendesk.d.ts.map +1 -0
- package/dist/services/zendesk.js +148 -0
- package/dist/services/zendesk.js.map +1 -0
- package/dist/tools/script.d.ts +21 -0
- package/dist/tools/script.d.ts.map +1 -0
- package/dist/tools/script.js +136 -0
- package/dist/tools/script.js.map +1 -0
- package/package.json +98 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Integration
|
|
3
|
+
*
|
|
4
|
+
* Browser automation for web scraping, testing, and automation tasks.
|
|
5
|
+
* Supports Chromium, Firefox, and WebKit browsers.
|
|
6
|
+
*/
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Playwright Client
|
|
9
|
+
// ============================================================================
|
|
10
|
+
/**
|
|
11
|
+
* Playwright browser automation client with session persistence and AI support
|
|
12
|
+
*/
|
|
13
|
+
export class PlaywrightClient {
|
|
14
|
+
playwright = null;
|
|
15
|
+
browser = null;
|
|
16
|
+
context = null;
|
|
17
|
+
page = null;
|
|
18
|
+
config;
|
|
19
|
+
stagehand = null;
|
|
20
|
+
aiBrowser = null;
|
|
21
|
+
sessionPath = null;
|
|
22
|
+
constructor(config = {}) {
|
|
23
|
+
this.config = {
|
|
24
|
+
browserType: 'chromium',
|
|
25
|
+
headless: true,
|
|
26
|
+
timeout: 30000,
|
|
27
|
+
viewport: { width: 1280, height: 720 },
|
|
28
|
+
sessionsDir: './sessions',
|
|
29
|
+
autoSaveSession: false,
|
|
30
|
+
...config,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Get the session file path for a given session ID
|
|
35
|
+
*/
|
|
36
|
+
getSessionPath(sessionId) {
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
const path = require('path');
|
|
39
|
+
const sessionsDir = this.config.sessionsDir || './sessions';
|
|
40
|
+
// Ensure sessions directory exists
|
|
41
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
42
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
return path.join(sessionsDir, `${sessionId}.json`);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Initialize and launch the browser
|
|
48
|
+
*/
|
|
49
|
+
async launch() {
|
|
50
|
+
if (this.browser)
|
|
51
|
+
return;
|
|
52
|
+
// Dynamically import playwright
|
|
53
|
+
this.playwright = await import('playwright');
|
|
54
|
+
const browserType = this.config.browserType || 'chromium';
|
|
55
|
+
const launchOptions = {
|
|
56
|
+
headless: this.config.headless,
|
|
57
|
+
slowMo: this.config.slowMo,
|
|
58
|
+
timeout: this.config.timeout,
|
|
59
|
+
};
|
|
60
|
+
if (this.config.proxy) {
|
|
61
|
+
launchOptions.proxy = this.config.proxy;
|
|
62
|
+
}
|
|
63
|
+
// Connect to existing browser or launch new one
|
|
64
|
+
if (this.config.wsEndpoint) {
|
|
65
|
+
this.browser = await this.playwright[browserType].connect(this.config.wsEndpoint);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
this.browser = await this.playwright[browserType].launch(launchOptions);
|
|
69
|
+
}
|
|
70
|
+
// Create browser context with options
|
|
71
|
+
const contextOptions = {
|
|
72
|
+
viewport: this.config.viewport,
|
|
73
|
+
userAgent: this.config.userAgent,
|
|
74
|
+
locale: this.config.locale,
|
|
75
|
+
timezoneId: this.config.timezoneId,
|
|
76
|
+
geolocation: this.config.geolocation,
|
|
77
|
+
permissions: this.config.permissions,
|
|
78
|
+
ignoreHTTPSErrors: this.config.ignoreHTTPSErrors,
|
|
79
|
+
extraHTTPHeaders: this.config.extraHTTPHeaders,
|
|
80
|
+
recordVideo: this.config.recordVideo,
|
|
81
|
+
};
|
|
82
|
+
// Load storage state from session or file
|
|
83
|
+
if (this.config.sessionId) {
|
|
84
|
+
const sessionPath = this.getSessionPath(this.config.sessionId);
|
|
85
|
+
const fs = require('fs');
|
|
86
|
+
if (fs.existsSync(sessionPath)) {
|
|
87
|
+
contextOptions.storageState = sessionPath;
|
|
88
|
+
this.sessionPath = sessionPath;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else if (this.config.storageState) {
|
|
92
|
+
contextOptions.storageState = this.config.storageState;
|
|
93
|
+
this.sessionPath = this.config.storageState;
|
|
94
|
+
}
|
|
95
|
+
// Handle device emulation
|
|
96
|
+
if (this.config.deviceName && this.playwright.devices[this.config.deviceName]) {
|
|
97
|
+
Object.assign(contextOptions, this.playwright.devices[this.config.deviceName]);
|
|
98
|
+
}
|
|
99
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
100
|
+
this.page = await this.context.newPage();
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Ensure browser is launched
|
|
104
|
+
*/
|
|
105
|
+
async ensureLaunched() {
|
|
106
|
+
if (!this.page) {
|
|
107
|
+
await this.launch();
|
|
108
|
+
}
|
|
109
|
+
return this.page;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Navigate to a URL
|
|
113
|
+
*/
|
|
114
|
+
async navigate(options) {
|
|
115
|
+
const page = await this.ensureLaunched();
|
|
116
|
+
await page.goto(options.url, {
|
|
117
|
+
waitUntil: options.waitUntil || 'load',
|
|
118
|
+
timeout: options.timeout,
|
|
119
|
+
referer: options.referer,
|
|
120
|
+
});
|
|
121
|
+
return {
|
|
122
|
+
url: page.url(),
|
|
123
|
+
title: await page.title(),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Click an element
|
|
128
|
+
*/
|
|
129
|
+
async click(options) {
|
|
130
|
+
const page = await this.ensureLaunched();
|
|
131
|
+
await page.click(options.selector, {
|
|
132
|
+
button: options.button,
|
|
133
|
+
clickCount: options.clickCount,
|
|
134
|
+
delay: options.delay,
|
|
135
|
+
modifiers: options.modifiers,
|
|
136
|
+
position: options.position,
|
|
137
|
+
force: options.force,
|
|
138
|
+
timeout: options.timeout,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Double click an element
|
|
143
|
+
*/
|
|
144
|
+
async dblclick(options) {
|
|
145
|
+
const page = await this.ensureLaunched();
|
|
146
|
+
await page.dblclick(options.selector, {
|
|
147
|
+
button: options.button,
|
|
148
|
+
delay: options.delay,
|
|
149
|
+
modifiers: options.modifiers,
|
|
150
|
+
position: options.position,
|
|
151
|
+
force: options.force,
|
|
152
|
+
timeout: options.timeout,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Type text into an element
|
|
157
|
+
*/
|
|
158
|
+
async type(options) {
|
|
159
|
+
const page = await this.ensureLaunched();
|
|
160
|
+
if (options.clear) {
|
|
161
|
+
await page.fill(options.selector, '');
|
|
162
|
+
}
|
|
163
|
+
await page.type(options.selector, options.text, {
|
|
164
|
+
delay: options.delay,
|
|
165
|
+
timeout: options.timeout,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Fill an input element with a value
|
|
170
|
+
*/
|
|
171
|
+
async fill(options) {
|
|
172
|
+
const page = await this.ensureLaunched();
|
|
173
|
+
await page.fill(options.selector, options.value, {
|
|
174
|
+
force: options.force,
|
|
175
|
+
timeout: options.timeout,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Select options from a dropdown
|
|
180
|
+
*/
|
|
181
|
+
async select(options) {
|
|
182
|
+
const page = await this.ensureLaunched();
|
|
183
|
+
const values = Array.isArray(options.values) ? options.values : [options.values];
|
|
184
|
+
return page.selectOption(options.selector, values, {
|
|
185
|
+
timeout: options.timeout,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Check a checkbox
|
|
190
|
+
*/
|
|
191
|
+
async check(selector, options) {
|
|
192
|
+
const page = await this.ensureLaunched();
|
|
193
|
+
await page.check(selector, options);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Uncheck a checkbox
|
|
197
|
+
*/
|
|
198
|
+
async uncheck(selector, options) {
|
|
199
|
+
const page = await this.ensureLaunched();
|
|
200
|
+
await page.uncheck(selector, options);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Hover over an element
|
|
204
|
+
*/
|
|
205
|
+
async hover(selector, options) {
|
|
206
|
+
const page = await this.ensureLaunched();
|
|
207
|
+
await page.hover(selector, options);
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Focus an element
|
|
211
|
+
*/
|
|
212
|
+
async focus(selector, options) {
|
|
213
|
+
const page = await this.ensureLaunched();
|
|
214
|
+
await page.focus(selector, options);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Press a key or key combination
|
|
218
|
+
*/
|
|
219
|
+
async press(selector, key, options) {
|
|
220
|
+
const page = await this.ensureLaunched();
|
|
221
|
+
await page.press(selector, key, options);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Press keyboard keys without focusing an element
|
|
225
|
+
*/
|
|
226
|
+
async keyboard(key, options) {
|
|
227
|
+
const page = await this.ensureLaunched();
|
|
228
|
+
await page.keyboard.press(key, options);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Take a screenshot
|
|
232
|
+
*/
|
|
233
|
+
async screenshot(options = {}) {
|
|
234
|
+
const page = await this.ensureLaunched();
|
|
235
|
+
let element = null;
|
|
236
|
+
if (options.selector) {
|
|
237
|
+
element = await page.$(options.selector);
|
|
238
|
+
if (!element) {
|
|
239
|
+
throw new Error(`Element not found: ${options.selector}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const screenshotOptions = {
|
|
243
|
+
path: options.path,
|
|
244
|
+
type: options.type || 'png',
|
|
245
|
+
quality: options.type === 'jpeg' ? options.quality : undefined,
|
|
246
|
+
fullPage: options.fullPage,
|
|
247
|
+
clip: options.clip,
|
|
248
|
+
omitBackground: options.omitBackground,
|
|
249
|
+
};
|
|
250
|
+
const buffer = element
|
|
251
|
+
? await element.screenshot(screenshotOptions)
|
|
252
|
+
: await page.screenshot(screenshotOptions);
|
|
253
|
+
return {
|
|
254
|
+
data: buffer.toString('base64'),
|
|
255
|
+
path: options.path,
|
|
256
|
+
type: options.type || 'png',
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Generate a PDF of the page (Chromium only)
|
|
261
|
+
*/
|
|
262
|
+
async pdf(options = {}) {
|
|
263
|
+
const page = await this.ensureLaunched();
|
|
264
|
+
const buffer = await page.pdf({
|
|
265
|
+
path: options.path,
|
|
266
|
+
format: options.format,
|
|
267
|
+
scale: options.scale,
|
|
268
|
+
displayHeaderFooter: options.displayHeaderFooter,
|
|
269
|
+
headerTemplate: options.headerTemplate,
|
|
270
|
+
footerTemplate: options.footerTemplate,
|
|
271
|
+
printBackground: options.printBackground,
|
|
272
|
+
landscape: options.landscape,
|
|
273
|
+
pageRanges: options.pageRanges,
|
|
274
|
+
width: options.width,
|
|
275
|
+
height: options.height,
|
|
276
|
+
margin: options.margin,
|
|
277
|
+
});
|
|
278
|
+
return {
|
|
279
|
+
data: buffer.toString('base64'),
|
|
280
|
+
path: options.path,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Evaluate JavaScript in the page context
|
|
285
|
+
*/
|
|
286
|
+
async evaluate(options) {
|
|
287
|
+
const page = await this.ensureLaunched();
|
|
288
|
+
// For simple expressions, evaluate directly
|
|
289
|
+
// For function expressions, wrap and call
|
|
290
|
+
if (options.expression.trim().startsWith('(') || options.expression.trim().startsWith('function')) {
|
|
291
|
+
// It's a function expression, evaluate it directly
|
|
292
|
+
return page.evaluate(options.expression, options.args);
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Wrap expression in a function
|
|
296
|
+
const wrappedExpression = `() => (${options.expression})`;
|
|
297
|
+
return page.evaluate(wrappedExpression);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Wait for various conditions
|
|
302
|
+
*/
|
|
303
|
+
async wait(options) {
|
|
304
|
+
const page = await this.ensureLaunched();
|
|
305
|
+
if (options.selector) {
|
|
306
|
+
await page.waitForSelector(options.selector, {
|
|
307
|
+
state: options.state,
|
|
308
|
+
timeout: options.timeout,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
else if (options.url) {
|
|
312
|
+
await page.waitForURL(options.url, { timeout: options.timeout });
|
|
313
|
+
}
|
|
314
|
+
else if (options.function) {
|
|
315
|
+
// Wrap the function expression for evaluation
|
|
316
|
+
const wrappedFn = `() => (${options.function})`;
|
|
317
|
+
await page.waitForFunction(wrappedFn, { timeout: options.timeout });
|
|
318
|
+
}
|
|
319
|
+
else if (options.loadState) {
|
|
320
|
+
await page.waitForLoadState(options.loadState, { timeout: options.timeout });
|
|
321
|
+
}
|
|
322
|
+
else if (options.networkIdle) {
|
|
323
|
+
await page.waitForLoadState('networkidle', { timeout: options.timeout });
|
|
324
|
+
}
|
|
325
|
+
else if (options.timeout) {
|
|
326
|
+
await page.waitForTimeout(options.timeout);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Extract data from the page
|
|
331
|
+
*/
|
|
332
|
+
async extract(options) {
|
|
333
|
+
const page = await this.ensureLaunched();
|
|
334
|
+
const elements = await page.$$(options.selector);
|
|
335
|
+
if (elements.length === 0) {
|
|
336
|
+
return { data: options.all ? [] : null, count: 0 };
|
|
337
|
+
}
|
|
338
|
+
const extractElement = async (el) => {
|
|
339
|
+
const result = {};
|
|
340
|
+
if (options.text) {
|
|
341
|
+
result.text = await el.textContent();
|
|
342
|
+
}
|
|
343
|
+
if (options.html) {
|
|
344
|
+
result.html = await el.innerHTML();
|
|
345
|
+
}
|
|
346
|
+
if (options.attributes) {
|
|
347
|
+
for (const attr of options.attributes) {
|
|
348
|
+
result[attr] = await el.getAttribute(attr);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
if (options.properties) {
|
|
352
|
+
for (const prop of options.properties) {
|
|
353
|
+
result[prop] = await el.evaluate((e, p) => e[p], prop);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// If only one type of data requested, return it directly
|
|
357
|
+
const keys = Object.keys(result);
|
|
358
|
+
if (keys.length === 1) {
|
|
359
|
+
return result[keys[0]];
|
|
360
|
+
}
|
|
361
|
+
return result;
|
|
362
|
+
};
|
|
363
|
+
if (options.all) {
|
|
364
|
+
const data = await Promise.all(elements.map(extractElement));
|
|
365
|
+
return { data, count: elements.length };
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
const data = await extractElement(elements[0]);
|
|
369
|
+
return { data, count: 1 };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Fill a form with multiple fields
|
|
374
|
+
*/
|
|
375
|
+
async fillForm(options) {
|
|
376
|
+
const page = await this.ensureLaunched();
|
|
377
|
+
const formSelector = options.formSelector || 'form';
|
|
378
|
+
for (const [name, value] of Object.entries(options.fields)) {
|
|
379
|
+
const selector = `${formSelector} [name="${name}"]`;
|
|
380
|
+
if (typeof value === 'boolean') {
|
|
381
|
+
if (value) {
|
|
382
|
+
await page.check(selector);
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
await page.uncheck(selector);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
else if (Array.isArray(value)) {
|
|
389
|
+
await page.selectOption(selector, value);
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
await page.fill(selector, value);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (options.submit) {
|
|
396
|
+
await page.click(`${formSelector} [type="submit"]`);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get or set cookies
|
|
401
|
+
*/
|
|
402
|
+
async cookies(options = {}) {
|
|
403
|
+
const context = this.context;
|
|
404
|
+
if (!context) {
|
|
405
|
+
await this.ensureLaunched();
|
|
406
|
+
}
|
|
407
|
+
if (options.cookies) {
|
|
408
|
+
await this.context.addCookies(options.cookies);
|
|
409
|
+
}
|
|
410
|
+
return this.context.cookies(options.urls);
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Clear cookies
|
|
414
|
+
*/
|
|
415
|
+
async clearCookies() {
|
|
416
|
+
if (this.context) {
|
|
417
|
+
await this.context.clearCookies();
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Manage local and session storage
|
|
422
|
+
*/
|
|
423
|
+
async storage(options) {
|
|
424
|
+
const page = await this.ensureLaunched();
|
|
425
|
+
const result = {};
|
|
426
|
+
// Set local storage
|
|
427
|
+
if (options.localStorage) {
|
|
428
|
+
await page.evaluate((items) => {
|
|
429
|
+
for (const [key, value] of Object.entries(items)) {
|
|
430
|
+
localStorage.setItem(key, value);
|
|
431
|
+
}
|
|
432
|
+
}, options.localStorage);
|
|
433
|
+
}
|
|
434
|
+
// Set session storage
|
|
435
|
+
if (options.sessionStorage) {
|
|
436
|
+
await page.evaluate((items) => {
|
|
437
|
+
for (const [key, value] of Object.entries(items)) {
|
|
438
|
+
sessionStorage.setItem(key, value);
|
|
439
|
+
}
|
|
440
|
+
}, options.sessionStorage);
|
|
441
|
+
}
|
|
442
|
+
// Get storage
|
|
443
|
+
if (options.getStorage === 'local' || options.getStorage === 'both') {
|
|
444
|
+
result.localStorage = await page.evaluate(() => {
|
|
445
|
+
const items = {};
|
|
446
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
447
|
+
const key = localStorage.key(i);
|
|
448
|
+
if (key)
|
|
449
|
+
items[key] = localStorage.getItem(key) || '';
|
|
450
|
+
}
|
|
451
|
+
return items;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
if (options.getStorage === 'session' || options.getStorage === 'both') {
|
|
455
|
+
result.sessionStorage = await page.evaluate(() => {
|
|
456
|
+
const items = {};
|
|
457
|
+
for (let i = 0; i < sessionStorage.length; i++) {
|
|
458
|
+
const key = sessionStorage.key(i);
|
|
459
|
+
if (key)
|
|
460
|
+
items[key] = sessionStorage.getItem(key) || '';
|
|
461
|
+
}
|
|
462
|
+
return items;
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Block network requests matching patterns
|
|
469
|
+
*/
|
|
470
|
+
async blockRequests(patterns) {
|
|
471
|
+
const page = await this.ensureLaunched();
|
|
472
|
+
await page.route((url) => patterns.some((p) => url.href.includes(p)), (route) => route.abort());
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Intercept and modify network requests
|
|
476
|
+
*/
|
|
477
|
+
async interceptRequests(handler) {
|
|
478
|
+
const page = await this.ensureLaunched();
|
|
479
|
+
await page.route('**/*', handler);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Get page content
|
|
483
|
+
*/
|
|
484
|
+
async content() {
|
|
485
|
+
const page = await this.ensureLaunched();
|
|
486
|
+
return page.content();
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Get page info
|
|
490
|
+
*/
|
|
491
|
+
async pageInfo(includeContent = false) {
|
|
492
|
+
const page = await this.ensureLaunched();
|
|
493
|
+
return {
|
|
494
|
+
url: page.url(),
|
|
495
|
+
title: await page.title(),
|
|
496
|
+
content: includeContent ? await page.content() : undefined,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Go back in browser history
|
|
501
|
+
*/
|
|
502
|
+
async goBack(options) {
|
|
503
|
+
const page = await this.ensureLaunched();
|
|
504
|
+
await page.goBack(options);
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Go forward in browser history
|
|
508
|
+
*/
|
|
509
|
+
async goForward(options) {
|
|
510
|
+
const page = await this.ensureLaunched();
|
|
511
|
+
await page.goForward(options);
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* Reload the page
|
|
515
|
+
*/
|
|
516
|
+
async reload(options) {
|
|
517
|
+
const page = await this.ensureLaunched();
|
|
518
|
+
await page.reload(options);
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Open a new page/tab
|
|
522
|
+
*/
|
|
523
|
+
async newPage() {
|
|
524
|
+
if (!this.context) {
|
|
525
|
+
await this.ensureLaunched();
|
|
526
|
+
}
|
|
527
|
+
this.page = await this.context.newPage();
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get all open pages
|
|
531
|
+
*/
|
|
532
|
+
async getPages() {
|
|
533
|
+
if (!this.context) {
|
|
534
|
+
return [];
|
|
535
|
+
}
|
|
536
|
+
const pages = this.context.pages();
|
|
537
|
+
return Promise.all(pages.map(async (p) => ({
|
|
538
|
+
url: p.url(),
|
|
539
|
+
title: await p.title(),
|
|
540
|
+
})));
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Switch to a page by index or URL
|
|
544
|
+
*/
|
|
545
|
+
async switchToPage(indexOrUrl) {
|
|
546
|
+
if (!this.context) {
|
|
547
|
+
throw new Error('No browser context');
|
|
548
|
+
}
|
|
549
|
+
const pages = this.context.pages();
|
|
550
|
+
let targetPage;
|
|
551
|
+
if (typeof indexOrUrl === 'number') {
|
|
552
|
+
targetPage = pages[indexOrUrl];
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
targetPage = pages.find((p) => p.url().includes(indexOrUrl));
|
|
556
|
+
}
|
|
557
|
+
if (!targetPage) {
|
|
558
|
+
throw new Error(`Page not found: ${indexOrUrl}`);
|
|
559
|
+
}
|
|
560
|
+
this.page = targetPage;
|
|
561
|
+
return {
|
|
562
|
+
url: this.page.url(),
|
|
563
|
+
title: await this.page.title(),
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Close the current page
|
|
568
|
+
*/
|
|
569
|
+
async closePage() {
|
|
570
|
+
if (this.page) {
|
|
571
|
+
await this.page.close();
|
|
572
|
+
const pages = this.context?.pages() || [];
|
|
573
|
+
this.page = pages.length > 0 ? pages[pages.length - 1] : null;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Upload a file to an input element
|
|
578
|
+
*/
|
|
579
|
+
async uploadFile(selector, files) {
|
|
580
|
+
const page = await this.ensureLaunched();
|
|
581
|
+
await page.setInputFiles(selector, files);
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Download a file
|
|
585
|
+
*/
|
|
586
|
+
async download(options) {
|
|
587
|
+
const page = await this.ensureLaunched();
|
|
588
|
+
const downloadPromise = page.waitForEvent('download');
|
|
589
|
+
if (options.selector) {
|
|
590
|
+
await page.click(options.selector);
|
|
591
|
+
}
|
|
592
|
+
else if (options.url) {
|
|
593
|
+
await page.goto(options.url);
|
|
594
|
+
}
|
|
595
|
+
const download = await downloadPromise;
|
|
596
|
+
const path = options.path || (await download.path());
|
|
597
|
+
if (options.path) {
|
|
598
|
+
await download.saveAs(options.path);
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
path: path || '',
|
|
602
|
+
suggestedFilename: download.suggestedFilename(),
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Handle dialog (alert, confirm, prompt)
|
|
607
|
+
*/
|
|
608
|
+
async handleDialog(action, promptText) {
|
|
609
|
+
const page = await this.ensureLaunched();
|
|
610
|
+
page.once('dialog', async (dialog) => {
|
|
611
|
+
if (action === 'accept') {
|
|
612
|
+
await dialog.accept(promptText);
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
await dialog.dismiss();
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Emulate media type or color scheme
|
|
621
|
+
*/
|
|
622
|
+
async emulateMedia(options) {
|
|
623
|
+
const page = await this.ensureLaunched();
|
|
624
|
+
await page.emulateMedia(options);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Close the browser
|
|
628
|
+
*/
|
|
629
|
+
async close() {
|
|
630
|
+
// Auto-save session if configured
|
|
631
|
+
if (this.config.autoSaveSession && this.config.sessionId && this.context) {
|
|
632
|
+
await this.saveSession().catch(() => { });
|
|
633
|
+
}
|
|
634
|
+
if (this.page) {
|
|
635
|
+
await this.page.close().catch(() => { });
|
|
636
|
+
this.page = null;
|
|
637
|
+
}
|
|
638
|
+
if (this.context) {
|
|
639
|
+
await this.context.close().catch(() => { });
|
|
640
|
+
this.context = null;
|
|
641
|
+
}
|
|
642
|
+
if (this.browser) {
|
|
643
|
+
await this.browser.close().catch(() => { });
|
|
644
|
+
this.browser = null;
|
|
645
|
+
}
|
|
646
|
+
this.stagehand = null;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Get the underlying Playwright page for advanced usage
|
|
650
|
+
*/
|
|
651
|
+
getPage() {
|
|
652
|
+
return this.page;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Get the underlying Playwright browser for advanced usage
|
|
656
|
+
*/
|
|
657
|
+
getBrowser() {
|
|
658
|
+
return this.browser;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Get the underlying Playwright context for advanced usage
|
|
662
|
+
*/
|
|
663
|
+
getContext() {
|
|
664
|
+
return this.context;
|
|
665
|
+
}
|
|
666
|
+
// ==========================================================================
|
|
667
|
+
// Session Management
|
|
668
|
+
// ==========================================================================
|
|
669
|
+
/**
|
|
670
|
+
* Save the current session state (cookies, localStorage, sessionStorage)
|
|
671
|
+
*/
|
|
672
|
+
async saveSession(sessionIdOrPath) {
|
|
673
|
+
if (!this.context) {
|
|
674
|
+
throw new Error('No browser context to save session from');
|
|
675
|
+
}
|
|
676
|
+
let savePath;
|
|
677
|
+
if (sessionIdOrPath) {
|
|
678
|
+
// Check if it's a path or session ID
|
|
679
|
+
if (sessionIdOrPath.includes('/') || sessionIdOrPath.includes('\\') || sessionIdOrPath.endsWith('.json')) {
|
|
680
|
+
savePath = sessionIdOrPath;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
savePath = this.getSessionPath(sessionIdOrPath);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
else if (this.config.sessionId) {
|
|
687
|
+
savePath = this.getSessionPath(this.config.sessionId);
|
|
688
|
+
}
|
|
689
|
+
else if (this.sessionPath) {
|
|
690
|
+
savePath = this.sessionPath;
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
throw new Error('No session ID or path specified');
|
|
694
|
+
}
|
|
695
|
+
// Save the storage state
|
|
696
|
+
await this.context.storageState({ path: savePath });
|
|
697
|
+
this.sessionPath = savePath;
|
|
698
|
+
// Get domains from cookies
|
|
699
|
+
const cookies = await this.context.cookies();
|
|
700
|
+
const domains = [...new Set(cookies.map(c => c.domain))];
|
|
701
|
+
// Create session info
|
|
702
|
+
const sessionId = savePath.split('/').pop()?.replace('.json', '') || 'unknown';
|
|
703
|
+
const now = new Date().toISOString();
|
|
704
|
+
const sessionInfo = {
|
|
705
|
+
id: sessionId,
|
|
706
|
+
path: savePath,
|
|
707
|
+
createdAt: now,
|
|
708
|
+
lastUsedAt: now,
|
|
709
|
+
domains,
|
|
710
|
+
};
|
|
711
|
+
return sessionInfo;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Load a session state from file
|
|
715
|
+
*/
|
|
716
|
+
async loadSession(sessionIdOrPath) {
|
|
717
|
+
const fs = require('fs');
|
|
718
|
+
let loadPath;
|
|
719
|
+
// Check if it's a path or session ID
|
|
720
|
+
if (sessionIdOrPath.includes('/') || sessionIdOrPath.includes('\\') || sessionIdOrPath.endsWith('.json')) {
|
|
721
|
+
loadPath = sessionIdOrPath;
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
loadPath = this.getSessionPath(sessionIdOrPath);
|
|
725
|
+
}
|
|
726
|
+
if (!fs.existsSync(loadPath)) {
|
|
727
|
+
throw new Error(`Session file not found: ${loadPath}`);
|
|
728
|
+
}
|
|
729
|
+
// If browser is already running, we need to create a new context
|
|
730
|
+
if (this.browser) {
|
|
731
|
+
// Close existing context
|
|
732
|
+
if (this.context) {
|
|
733
|
+
await this.context.close();
|
|
734
|
+
}
|
|
735
|
+
// Create new context with session state
|
|
736
|
+
const contextOptions = {
|
|
737
|
+
viewport: this.config.viewport,
|
|
738
|
+
userAgent: this.config.userAgent,
|
|
739
|
+
locale: this.config.locale,
|
|
740
|
+
timezoneId: this.config.timezoneId,
|
|
741
|
+
storageState: loadPath,
|
|
742
|
+
};
|
|
743
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
744
|
+
this.page = await this.context.newPage();
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
// Store path for when browser is launched
|
|
748
|
+
this.config.storageState = loadPath;
|
|
749
|
+
}
|
|
750
|
+
this.sessionPath = loadPath;
|
|
751
|
+
// Read session to get info
|
|
752
|
+
const sessionData = JSON.parse(fs.readFileSync(loadPath, 'utf-8'));
|
|
753
|
+
const domains = [...new Set((sessionData.cookies || []).map((c) => c.domain))];
|
|
754
|
+
const sessionId = loadPath.split('/').pop()?.replace('.json', '') || 'unknown';
|
|
755
|
+
return {
|
|
756
|
+
id: sessionId,
|
|
757
|
+
path: loadPath,
|
|
758
|
+
createdAt: new Date().toISOString(),
|
|
759
|
+
lastUsedAt: new Date().toISOString(),
|
|
760
|
+
domains: domains,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* List all saved sessions
|
|
765
|
+
*/
|
|
766
|
+
async listSessions() {
|
|
767
|
+
const fs = require('fs');
|
|
768
|
+
const path = require('path');
|
|
769
|
+
const sessionsDir = this.config.sessionsDir || './sessions';
|
|
770
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
771
|
+
return [];
|
|
772
|
+
}
|
|
773
|
+
const files = fs.readdirSync(sessionsDir).filter((f) => f.endsWith('.json'));
|
|
774
|
+
const sessions = [];
|
|
775
|
+
for (const file of files) {
|
|
776
|
+
const filePath = path.join(sessionsDir, file);
|
|
777
|
+
try {
|
|
778
|
+
const stat = fs.statSync(filePath);
|
|
779
|
+
const sessionData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
780
|
+
const domains = [...new Set((sessionData.cookies || []).map((c) => c.domain))];
|
|
781
|
+
sessions.push({
|
|
782
|
+
id: file.replace('.json', ''),
|
|
783
|
+
path: filePath,
|
|
784
|
+
createdAt: stat.birthtime.toISOString(),
|
|
785
|
+
lastUsedAt: stat.mtime.toISOString(),
|
|
786
|
+
domains: domains,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
catch {
|
|
790
|
+
// Skip invalid files
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return sessions;
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* Delete a saved session
|
|
797
|
+
*/
|
|
798
|
+
async deleteSession(sessionId) {
|
|
799
|
+
const fs = require('fs');
|
|
800
|
+
const sessionPath = this.getSessionPath(sessionId);
|
|
801
|
+
if (fs.existsSync(sessionPath)) {
|
|
802
|
+
fs.unlinkSync(sessionPath);
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
/**
|
|
808
|
+
* Check if a session exists
|
|
809
|
+
*/
|
|
810
|
+
hasSession(sessionId) {
|
|
811
|
+
const fs = require('fs');
|
|
812
|
+
return fs.existsSync(this.getSessionPath(sessionId));
|
|
813
|
+
}
|
|
814
|
+
// ==========================================================================
|
|
815
|
+
// AI-Powered Automation
|
|
816
|
+
// ==========================================================================
|
|
817
|
+
/**
|
|
818
|
+
* Initialize custom AI browser automation (Copilot/Claude Code)
|
|
819
|
+
*/
|
|
820
|
+
async initAIBrowser() {
|
|
821
|
+
if (this.aiBrowser) {
|
|
822
|
+
return this.aiBrowser;
|
|
823
|
+
}
|
|
824
|
+
if (!this.config.enableAI) {
|
|
825
|
+
throw new Error('AI automation is not enabled. Set enableAI: true in config.');
|
|
826
|
+
}
|
|
827
|
+
if (!this.config.aiBackend || this.config.aiBackend === 'stagehand') {
|
|
828
|
+
throw new Error('Custom AI backend not configured. Set aiBackend to "copilot" or "claude-code"');
|
|
829
|
+
}
|
|
830
|
+
if (!this.config.aiClient) {
|
|
831
|
+
throw new Error(`AI client not provided. Initialize ${this.config.aiBackend} client first.`);
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
const { AIBrowserClient } = await import('./ai-browser.js');
|
|
835
|
+
this.aiBrowser = new AIBrowserClient({
|
|
836
|
+
backend: this.config.aiBackend,
|
|
837
|
+
aiClient: this.config.aiClient,
|
|
838
|
+
playwrightClient: this,
|
|
839
|
+
debug: this.config.aiDebug,
|
|
840
|
+
});
|
|
841
|
+
return this.aiBrowser;
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
throw new Error(`Failed to initialize AI browser automation: ${error instanceof Error ? error.message : String(error)}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Initialize Stagehand for AI-powered automation (legacy support)
|
|
849
|
+
*/
|
|
850
|
+
async initStagehand() {
|
|
851
|
+
if (this.stagehand) {
|
|
852
|
+
return this.stagehand;
|
|
853
|
+
}
|
|
854
|
+
if (!this.config.enableAI) {
|
|
855
|
+
throw new Error('AI automation is not enabled. Set enableAI: true in config.');
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
// Dynamically import Stagehand (optional dependency)
|
|
859
|
+
// Use string variable to bypass TypeScript module resolution
|
|
860
|
+
const stagehandPackage = '@browserbasehq/stagehand';
|
|
861
|
+
let stagehandModule = null;
|
|
862
|
+
try {
|
|
863
|
+
stagehandModule = await import(stagehandPackage);
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
throw new Error('Stagehand is not installed. Install it with: npm install @browserbasehq/stagehand');
|
|
867
|
+
}
|
|
868
|
+
const { Stagehand } = stagehandModule;
|
|
869
|
+
// Ensure browser is launched
|
|
870
|
+
await this.ensureLaunched();
|
|
871
|
+
// Configure Stagehand
|
|
872
|
+
const stagehandConfig = {
|
|
873
|
+
env: 'LOCAL',
|
|
874
|
+
enableCaching: true,
|
|
875
|
+
debugDom: this.config.aiDebug,
|
|
876
|
+
};
|
|
877
|
+
// Set model configuration
|
|
878
|
+
if (this.config.aiProvider === 'anthropic') {
|
|
879
|
+
stagehandConfig.modelName = this.config.aiModel || 'claude-3-5-sonnet-latest';
|
|
880
|
+
stagehandConfig.modelClientOptions = {
|
|
881
|
+
apiKey: this.config.aiApiKey || process.env.ANTHROPIC_API_KEY,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// Default to OpenAI
|
|
886
|
+
stagehandConfig.modelName = this.config.aiModel || 'gpt-4o';
|
|
887
|
+
stagehandConfig.modelClientOptions = {
|
|
888
|
+
apiKey: this.config.aiApiKey || process.env.OPENAI_API_KEY,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
this.stagehand = new Stagehand(stagehandConfig);
|
|
892
|
+
await this.stagehand.init({ page: this.page });
|
|
893
|
+
return this.stagehand;
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
throw new Error(`Failed to initialize Stagehand AI. Make sure @browserbasehq/stagehand is installed: npm install @browserbasehq/stagehand\n` +
|
|
897
|
+
`Original error: ${error}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Perform an action using natural language (AI-powered)
|
|
902
|
+
*
|
|
903
|
+
* @example
|
|
904
|
+
* await browser.act({ instruction: 'Click the login button' })
|
|
905
|
+
* await browser.act({ instruction: 'Fill in the email field with test@example.com' })
|
|
906
|
+
*/
|
|
907
|
+
async act(options) {
|
|
908
|
+
// Use custom AI backend if configured
|
|
909
|
+
if (this.config.aiBackend && this.config.aiBackend !== 'stagehand') {
|
|
910
|
+
const aiBrowser = await this.initAIBrowser();
|
|
911
|
+
try {
|
|
912
|
+
const result = await aiBrowser.act({ action: options.instruction });
|
|
913
|
+
return {
|
|
914
|
+
success: result.success,
|
|
915
|
+
action: result.action,
|
|
916
|
+
element: result.message,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
catch (error) {
|
|
920
|
+
if (options.optional) {
|
|
921
|
+
return {
|
|
922
|
+
success: false,
|
|
923
|
+
error: String(error),
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
throw error;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// Fall back to Stagehand
|
|
930
|
+
const stagehand = await this.initStagehand();
|
|
931
|
+
try {
|
|
932
|
+
const result = await stagehand.act({
|
|
933
|
+
action: options.instruction,
|
|
934
|
+
});
|
|
935
|
+
return {
|
|
936
|
+
success: result.success !== false,
|
|
937
|
+
action: result.action || options.instruction,
|
|
938
|
+
element: result.message,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
if (options.optional) {
|
|
943
|
+
return {
|
|
944
|
+
success: false,
|
|
945
|
+
error: String(error),
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
throw error;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* Observe available actions on the page (AI-powered)
|
|
953
|
+
*
|
|
954
|
+
* @example
|
|
955
|
+
* const result = await browser.observe({ instruction: 'Find all buttons' })
|
|
956
|
+
*/
|
|
957
|
+
async observe(options = {}) {
|
|
958
|
+
// Use custom AI backend if configured
|
|
959
|
+
if (this.config.aiBackend && this.config.aiBackend !== 'stagehand') {
|
|
960
|
+
const aiBrowser = await this.initAIBrowser();
|
|
961
|
+
const elements = await aiBrowser.observe({ instruction: options.instruction });
|
|
962
|
+
return {
|
|
963
|
+
elements: elements.map(el => ({
|
|
964
|
+
description: el.description || '',
|
|
965
|
+
selector: el.selector || '',
|
|
966
|
+
tagName: el.tagName || 'unknown',
|
|
967
|
+
text: el.text,
|
|
968
|
+
actions: el.actions || ['click'],
|
|
969
|
+
})),
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
// Fall back to Stagehand
|
|
973
|
+
const stagehand = await this.initStagehand();
|
|
974
|
+
const result = await stagehand.observe({
|
|
975
|
+
instruction: options.instruction,
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
elements: (result || []).map(el => ({
|
|
979
|
+
description: el.description || '',
|
|
980
|
+
selector: el.selector || '',
|
|
981
|
+
tagName: el.tagName || 'unknown',
|
|
982
|
+
actions: el.actions || ['click'],
|
|
983
|
+
})),
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Extract structured data from the page using AI
|
|
988
|
+
*
|
|
989
|
+
* @example
|
|
990
|
+
* const data = await browser.aiExtract({
|
|
991
|
+
* instruction: 'Extract the product name and price',
|
|
992
|
+
* schema: { name: 'string', price: 'number' }
|
|
993
|
+
* })
|
|
994
|
+
*/
|
|
995
|
+
async aiExtract(options) {
|
|
996
|
+
// Use custom AI backend if configured
|
|
997
|
+
if (this.config.aiBackend && this.config.aiBackend !== 'stagehand') {
|
|
998
|
+
const aiBrowser = await this.initAIBrowser();
|
|
999
|
+
const result = await aiBrowser.aiExtract({
|
|
1000
|
+
instruction: options.instruction,
|
|
1001
|
+
schema: options.schema,
|
|
1002
|
+
});
|
|
1003
|
+
return result.data;
|
|
1004
|
+
}
|
|
1005
|
+
// Fall back to Stagehand
|
|
1006
|
+
const stagehand = await this.initStagehand();
|
|
1007
|
+
// Convert simple schema description to format Stagehand expects
|
|
1008
|
+
let schema = undefined;
|
|
1009
|
+
if (options.schema) {
|
|
1010
|
+
// For now, pass the schema description as-is
|
|
1011
|
+
// Users can use Zod schemas if they import @browserbasehq/stagehand directly
|
|
1012
|
+
schema = options.schema;
|
|
1013
|
+
}
|
|
1014
|
+
const result = await stagehand.extract({
|
|
1015
|
+
instruction: options.instruction,
|
|
1016
|
+
schema,
|
|
1017
|
+
});
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* Check if AI automation is available
|
|
1022
|
+
*/
|
|
1023
|
+
isAIEnabled() {
|
|
1024
|
+
return this.config.enableAI === true;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
// ============================================================================
|
|
1028
|
+
// SDK Initializer
|
|
1029
|
+
// ============================================================================
|
|
1030
|
+
export const PlaywrightInitializer = {
|
|
1031
|
+
async initialize(_module, config) {
|
|
1032
|
+
const options = config.options || {};
|
|
1033
|
+
// Helper to get option with fallback key
|
|
1034
|
+
const getOption = (key1, key2) => {
|
|
1035
|
+
const value = options[key1] ?? (key2 ? options[key2] : undefined);
|
|
1036
|
+
return value;
|
|
1037
|
+
};
|
|
1038
|
+
const playwrightConfig = {
|
|
1039
|
+
browserType: getOption('browser_type', 'browserType'),
|
|
1040
|
+
headless: getOption('headless'),
|
|
1041
|
+
slowMo: getOption('slow_mo', 'slowMo'),
|
|
1042
|
+
timeout: getOption('timeout'),
|
|
1043
|
+
viewport: getOption('viewport'),
|
|
1044
|
+
userAgent: getOption('user_agent', 'userAgent'),
|
|
1045
|
+
locale: getOption('locale'),
|
|
1046
|
+
timezoneId: getOption('timezone_id', 'timezoneId'),
|
|
1047
|
+
geolocation: getOption('geolocation'),
|
|
1048
|
+
permissions: getOption('permissions'),
|
|
1049
|
+
ignoreHTTPSErrors: getOption('ignore_https_errors', 'ignoreHTTPSErrors'),
|
|
1050
|
+
deviceName: getOption('device_name', 'deviceName'),
|
|
1051
|
+
proxy: getOption('proxy'),
|
|
1052
|
+
extraHTTPHeaders: getOption('extra_http_headers', 'extraHTTPHeaders'),
|
|
1053
|
+
recordVideo: getOption('record_video', 'recordVideo'),
|
|
1054
|
+
wsEndpoint: getOption('ws_endpoint', 'wsEndpoint'),
|
|
1055
|
+
// Session persistence
|
|
1056
|
+
storageState: getOption('storage_state', 'storageState'),
|
|
1057
|
+
sessionId: getOption('session_id', 'sessionId'),
|
|
1058
|
+
sessionsDir: getOption('sessions_dir', 'sessionsDir'),
|
|
1059
|
+
autoSaveSession: getOption('auto_save_session', 'autoSaveSession'),
|
|
1060
|
+
// AI automation
|
|
1061
|
+
enableAI: getOption('enable_ai', 'enableAI'),
|
|
1062
|
+
aiBackend: getOption('ai_backend', 'aiBackend'),
|
|
1063
|
+
aiClient: getOption('ai_client', 'aiClient'),
|
|
1064
|
+
aiProvider: getOption('ai_provider', 'aiProvider'),
|
|
1065
|
+
aiModel: getOption('ai_model', 'aiModel'),
|
|
1066
|
+
aiApiKey: getOption('ai_api_key', 'aiApiKey'),
|
|
1067
|
+
aiDebug: getOption('ai_debug', 'aiDebug'),
|
|
1068
|
+
};
|
|
1069
|
+
const client = new PlaywrightClient(playwrightConfig);
|
|
1070
|
+
return client;
|
|
1071
|
+
},
|
|
1072
|
+
};
|
|
1073
|
+
// ============================================================================
|
|
1074
|
+
// Convenience Functions
|
|
1075
|
+
// ============================================================================
|
|
1076
|
+
/**
|
|
1077
|
+
* Create a Playwright client with default options
|
|
1078
|
+
*/
|
|
1079
|
+
export function createPlaywrightClient(config) {
|
|
1080
|
+
return new PlaywrightClient(config);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Quick web scraping helper
|
|
1084
|
+
*/
|
|
1085
|
+
export async function scrape(url, selectors, options) {
|
|
1086
|
+
const client = new PlaywrightClient(options);
|
|
1087
|
+
try {
|
|
1088
|
+
await client.navigate({ url });
|
|
1089
|
+
const result = {};
|
|
1090
|
+
for (const [key, selector] of Object.entries(selectors)) {
|
|
1091
|
+
const extracted = await client.extract({ selector, text: true, all: true });
|
|
1092
|
+
result[key] = extracted.data;
|
|
1093
|
+
}
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1096
|
+
finally {
|
|
1097
|
+
await client.close();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* Quick screenshot helper
|
|
1102
|
+
*/
|
|
1103
|
+
export async function screenshotUrl(url, options) {
|
|
1104
|
+
const { path, type, quality, fullPage, selector, clip, omitBackground, ...config } = options || {};
|
|
1105
|
+
const client = new PlaywrightClient(config);
|
|
1106
|
+
try {
|
|
1107
|
+
await client.navigate({ url });
|
|
1108
|
+
return client.screenshot({ path, type, quality, fullPage, selector, clip, omitBackground });
|
|
1109
|
+
}
|
|
1110
|
+
finally {
|
|
1111
|
+
await client.close();
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Quick PDF generation helper
|
|
1116
|
+
*/
|
|
1117
|
+
export async function pdfUrl(url, options) {
|
|
1118
|
+
const { path, format, scale, displayHeaderFooter, headerTemplate, footerTemplate, printBackground, landscape, pageRanges, width, height, margin, ...config } = options || {};
|
|
1119
|
+
const client = new PlaywrightClient({ ...config, browserType: 'chromium' }); // PDF only works in Chromium
|
|
1120
|
+
try {
|
|
1121
|
+
await client.navigate({ url });
|
|
1122
|
+
return client.pdf({
|
|
1123
|
+
path,
|
|
1124
|
+
format,
|
|
1125
|
+
scale,
|
|
1126
|
+
displayHeaderFooter,
|
|
1127
|
+
headerTemplate,
|
|
1128
|
+
footerTemplate,
|
|
1129
|
+
printBackground,
|
|
1130
|
+
landscape,
|
|
1131
|
+
pageRanges,
|
|
1132
|
+
width,
|
|
1133
|
+
height,
|
|
1134
|
+
margin,
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
finally {
|
|
1138
|
+
await client.close();
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
//# sourceMappingURL=playwright.js.map
|