slapify 0.0.16 → 0.0.17
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/dist/ai/interpreter.js +1 -331
- package/dist/browser/agent.js +1 -485
- package/dist/cli.js +1 -1553
- package/dist/config/loader.js +1 -305
- package/dist/index.js +1 -262
- package/dist/parser/flow.js +1 -117
- package/dist/perf/audit.js +1 -635
- package/dist/report/generator.js +1 -641
- package/dist/runner/index.js +1 -744
- package/dist/task/index.js +1 -4
- package/dist/task/report.js +1 -740
- package/dist/task/runner.js +1 -1362
- package/dist/task/session.js +1 -153
- package/dist/task/tools.js +1 -258
- package/dist/task/types.js +1 -2
- package/dist/types.js +1 -2
- package/package.json +6 -3
- package/dist/ai/interpreter.d.ts.map +0 -1
- package/dist/ai/interpreter.js.map +0 -1
- package/dist/browser/agent.d.ts.map +0 -1
- package/dist/browser/agent.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/parser/flow.d.ts.map +0 -1
- package/dist/parser/flow.js.map +0 -1
- package/dist/perf/audit.d.ts.map +0 -1
- package/dist/perf/audit.js.map +0 -1
- package/dist/report/generator.d.ts.map +0 -1
- package/dist/report/generator.js.map +0 -1
- package/dist/runner/index.d.ts.map +0 -1
- package/dist/runner/index.js.map +0 -1
- package/dist/task/index.d.ts.map +0 -1
- package/dist/task/index.js.map +0 -1
- package/dist/task/report.d.ts.map +0 -1
- package/dist/task/report.js.map +0 -1
- package/dist/task/runner.d.ts.map +0 -1
- package/dist/task/runner.js.map +0 -1
- package/dist/task/session.d.ts.map +0 -1
- package/dist/task/session.js.map +0 -1
- package/dist/task/tools.d.ts.map +0 -1
- package/dist/task/tools.js.map +0 -1
- package/dist/task/types.d.ts.map +0 -1
- package/dist/task/types.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js.map +0 -1
package/dist/browser/agent.js
CHANGED
|
@@ -1,485 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// Transient errors that should trigger automatic retry
|
|
3
|
-
const TRANSIENT_ERRORS = [
|
|
4
|
-
"Execution context was destroyed",
|
|
5
|
-
"Target closed",
|
|
6
|
-
"Navigation interrupted",
|
|
7
|
-
"Protocol error",
|
|
8
|
-
"Session closed",
|
|
9
|
-
"Page crashed",
|
|
10
|
-
"Frame was detached",
|
|
11
|
-
"Cannot find context",
|
|
12
|
-
];
|
|
13
|
-
/**
|
|
14
|
-
* Wrapper around agent-browser CLI
|
|
15
|
-
*/
|
|
16
|
-
export class BrowserAgent {
|
|
17
|
-
config;
|
|
18
|
-
isOpen = false;
|
|
19
|
-
constructor(config = {}) {
|
|
20
|
-
this.config = {
|
|
21
|
-
headless: true,
|
|
22
|
-
timeout: 30000,
|
|
23
|
-
viewport: { width: 1280, height: 720 },
|
|
24
|
-
...config,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Check if an error is transient and should be retried
|
|
29
|
-
*/
|
|
30
|
-
isTransientError(errorMessage) {
|
|
31
|
-
return TRANSIENT_ERRORS.some((e) => errorMessage.toLowerCase().includes(e.toLowerCase()));
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* Sleep for a given number of milliseconds
|
|
35
|
-
*/
|
|
36
|
-
sleep(ms) {
|
|
37
|
-
execSync(`sleep ${ms / 1000}`);
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Execute an agent-browser command with auto-retry for transient errors
|
|
41
|
-
*/
|
|
42
|
-
exec(command, args = [], retries = 2) {
|
|
43
|
-
const fullCommand = ["agent-browser", command, ...args].join(" ");
|
|
44
|
-
// Set up environment with executable path if configured
|
|
45
|
-
const env = { ...process.env };
|
|
46
|
-
if (this.config.executablePath) {
|
|
47
|
-
env.AGENT_BROWSER_EXECUTABLE_PATH = this.config.executablePath;
|
|
48
|
-
}
|
|
49
|
-
let lastError = null;
|
|
50
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
51
|
-
try {
|
|
52
|
-
const result = execSync(fullCommand, {
|
|
53
|
-
encoding: "utf-8",
|
|
54
|
-
timeout: this.config.timeout,
|
|
55
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
56
|
-
env,
|
|
57
|
-
});
|
|
58
|
-
return result.trim();
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
let errorMsg = error.message || error.stderr?.toString() || "";
|
|
62
|
-
// Filter out non-fatal daemon warnings
|
|
63
|
-
const isJustWarning = errorMsg.includes("daemon already running") ||
|
|
64
|
-
errorMsg.includes("--executable-path ignored");
|
|
65
|
-
// If we have stdout, return it (some commands output to stdout even on "error")
|
|
66
|
-
if (error.stdout) {
|
|
67
|
-
const stdout = error.stdout.toString().trim();
|
|
68
|
-
// If it's just a warning and we have stdout, return stdout
|
|
69
|
-
if (isJustWarning && stdout) {
|
|
70
|
-
return stdout;
|
|
71
|
-
}
|
|
72
|
-
// Check if stdout contains an actual error message
|
|
73
|
-
if (!this.isTransientError(stdout)) {
|
|
74
|
-
return stdout;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// If it's just a warning with no output, consider it success
|
|
78
|
-
if (isJustWarning) {
|
|
79
|
-
return "";
|
|
80
|
-
}
|
|
81
|
-
// Check if this is a transient error worth retrying
|
|
82
|
-
if (this.isTransientError(errorMsg) && attempt < retries) {
|
|
83
|
-
// Wait before retry (with exponential backoff)
|
|
84
|
-
this.sleep(500 * (attempt + 1));
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
lastError = new Error(`Browser command failed: ${fullCommand}\n${errorMsg}`);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
throw lastError || new Error(`Browser command failed: ${fullCommand}`);
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Check if agent-browser is installed
|
|
94
|
-
*/
|
|
95
|
-
static isInstalled() {
|
|
96
|
-
try {
|
|
97
|
-
execSync("agent-browser --version", { stdio: "pipe" });
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Install agent-browser and its browser
|
|
106
|
-
*/
|
|
107
|
-
static install() {
|
|
108
|
-
console.log("Installing agent-browser...");
|
|
109
|
-
execSync("npm install -g agent-browser", { stdio: "inherit" });
|
|
110
|
-
console.log("Installing browser...");
|
|
111
|
-
execSync("agent-browser install", { stdio: "inherit" });
|
|
112
|
-
}
|
|
113
|
-
hasInitialized = false;
|
|
114
|
-
/**
|
|
115
|
-
* Navigate to a URL
|
|
116
|
-
*/
|
|
117
|
-
async navigate(url) {
|
|
118
|
-
const args = [];
|
|
119
|
-
// On first navigation, close any existing browser to apply our settings
|
|
120
|
-
// This ensures headed mode and executable path are applied correctly
|
|
121
|
-
if (!this.hasInitialized &&
|
|
122
|
-
(this.config.headless === false || this.config.executablePath)) {
|
|
123
|
-
try {
|
|
124
|
-
this.exec("close", []);
|
|
125
|
-
// Give daemon time to fully shut down before reopening
|
|
126
|
-
this.sleep(1000);
|
|
127
|
-
}
|
|
128
|
-
catch {
|
|
129
|
-
// Ignore if no browser to close
|
|
130
|
-
}
|
|
131
|
-
this.hasInitialized = true;
|
|
132
|
-
}
|
|
133
|
-
if (this.config.headless === false) {
|
|
134
|
-
args.push("--headed");
|
|
135
|
-
}
|
|
136
|
-
// Use custom executable path if specified
|
|
137
|
-
if (this.config.executablePath) {
|
|
138
|
-
args.push("--executable-path", `"${this.config.executablePath}"`);
|
|
139
|
-
}
|
|
140
|
-
const doOpen = () => this.exec("open", [url, ...args]);
|
|
141
|
-
doOpen();
|
|
142
|
-
this.isOpen = true;
|
|
143
|
-
// If we're stuck on about:blank (e.g. daemon in bad state), retry open
|
|
144
|
-
const maxNavigateRetries = 2;
|
|
145
|
-
for (let r = 0; r < maxNavigateRetries; r++) {
|
|
146
|
-
this.sleep(1500);
|
|
147
|
-
try {
|
|
148
|
-
const currentUrl = this.exec("get", ["url"]).trim();
|
|
149
|
-
if (currentUrl &&
|
|
150
|
-
currentUrl !== "about:blank" &&
|
|
151
|
-
currentUrl !== "about:blank/") {
|
|
152
|
-
break;
|
|
153
|
-
}
|
|
154
|
-
// Still on about:blank - close and try open again
|
|
155
|
-
try {
|
|
156
|
-
this.exec("close", []);
|
|
157
|
-
this.sleep(1000);
|
|
158
|
-
}
|
|
159
|
-
catch {
|
|
160
|
-
// ignore
|
|
161
|
-
}
|
|
162
|
-
doOpen();
|
|
163
|
-
this.sleep(1500);
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
// Set viewport if specified
|
|
170
|
-
if (this.config.viewport) {
|
|
171
|
-
this.exec("set", [
|
|
172
|
-
"viewport",
|
|
173
|
-
String(this.config.viewport.width),
|
|
174
|
-
String(this.config.viewport.height),
|
|
175
|
-
]);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
/**
|
|
179
|
-
* Get page snapshot (accessibility tree)
|
|
180
|
-
*/
|
|
181
|
-
async snapshot(interactive = true) {
|
|
182
|
-
const args = interactive ? ["-i"] : [];
|
|
183
|
-
return this.exec("snapshot", args);
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Get page snapshot as JSON with refs
|
|
187
|
-
*/
|
|
188
|
-
async snapshotJson() {
|
|
189
|
-
const result = this.exec("snapshot", ["-i", "--json"]);
|
|
190
|
-
try {
|
|
191
|
-
const parsed = JSON.parse(result);
|
|
192
|
-
return {
|
|
193
|
-
snapshot: parsed.data?.snapshot || result,
|
|
194
|
-
refs: parsed.data?.refs || {},
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
return { snapshot: result, refs: {} };
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Click an element by ref or selector
|
|
203
|
-
*/
|
|
204
|
-
async click(selector) {
|
|
205
|
-
this.exec("click", [selector]);
|
|
206
|
-
}
|
|
207
|
-
/**
|
|
208
|
-
* Fill an input field
|
|
209
|
-
*/
|
|
210
|
-
async fill(selector, value) {
|
|
211
|
-
this.exec("fill", [selector, `"${value}"`]);
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Type text (appends to existing value)
|
|
215
|
-
*/
|
|
216
|
-
async type(selector, value) {
|
|
217
|
-
this.exec("type", [selector, `"${value}"`]);
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Press a key
|
|
221
|
-
*/
|
|
222
|
-
async press(key) {
|
|
223
|
-
this.exec("press", [key]);
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Hover over an element
|
|
227
|
-
*/
|
|
228
|
-
async hover(selector) {
|
|
229
|
-
this.exec("hover", [selector]);
|
|
230
|
-
}
|
|
231
|
-
/**
|
|
232
|
-
* Select dropdown option
|
|
233
|
-
*/
|
|
234
|
-
async select(selector, value) {
|
|
235
|
-
this.exec("select", [selector, value]);
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Scroll the page
|
|
239
|
-
*/
|
|
240
|
-
async scroll(direction, amount) {
|
|
241
|
-
const args = amount ? [direction, String(amount)] : [direction];
|
|
242
|
-
this.exec("scroll", args);
|
|
243
|
-
}
|
|
244
|
-
/**
|
|
245
|
-
* Wait for various conditions
|
|
246
|
-
*/
|
|
247
|
-
async wait(condition) {
|
|
248
|
-
if (typeof condition === "number") {
|
|
249
|
-
this.exec("wait", [String(condition)]);
|
|
250
|
-
}
|
|
251
|
-
else if (condition.startsWith("text=")) {
|
|
252
|
-
this.exec("wait", ["--text", `"${condition.substring(5)}"`]);
|
|
253
|
-
}
|
|
254
|
-
else if (condition.startsWith("url=")) {
|
|
255
|
-
this.exec("wait", ["--url", `"${condition.substring(4)}"`]);
|
|
256
|
-
}
|
|
257
|
-
else {
|
|
258
|
-
this.exec("wait", [condition]);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
/**
|
|
262
|
-
* Take a screenshot
|
|
263
|
-
*/
|
|
264
|
-
async screenshot(path, fullPage = false) {
|
|
265
|
-
const args = [];
|
|
266
|
-
if (path)
|
|
267
|
-
args.push(path);
|
|
268
|
-
if (fullPage)
|
|
269
|
-
args.push("--full");
|
|
270
|
-
return this.exec("screenshot", args);
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Get text content of an element
|
|
274
|
-
*/
|
|
275
|
-
async getText(selector) {
|
|
276
|
-
return this.exec("get", ["text", selector]);
|
|
277
|
-
}
|
|
278
|
-
/**
|
|
279
|
-
* Get current URL
|
|
280
|
-
*/
|
|
281
|
-
async getUrl() {
|
|
282
|
-
return this.exec("get", ["url"]);
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Get page title
|
|
286
|
-
*/
|
|
287
|
-
async getTitle() {
|
|
288
|
-
return this.exec("get", ["title"]);
|
|
289
|
-
}
|
|
290
|
-
/**
|
|
291
|
-
* Check if element is visible
|
|
292
|
-
*/
|
|
293
|
-
async isVisible(selector) {
|
|
294
|
-
const result = this.exec("is", ["visible", selector]);
|
|
295
|
-
return result.toLowerCase().includes("true");
|
|
296
|
-
}
|
|
297
|
-
/**
|
|
298
|
-
* Execute JavaScript
|
|
299
|
-
*/
|
|
300
|
-
async evaluate(script) {
|
|
301
|
-
return this.exec("eval", [`"${script.replace(/"/g, '\\"')}"`]);
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Set cookies
|
|
305
|
-
*/
|
|
306
|
-
async setCookie(name, value) {
|
|
307
|
-
// Escape special shell characters in cookie value
|
|
308
|
-
const escapedValue = `'${value.replace(/'/g, "'\\''")}'`;
|
|
309
|
-
this.exec("cookies", ["set", name, escapedValue]);
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Get all cookies
|
|
313
|
-
*/
|
|
314
|
-
async getCookies() {
|
|
315
|
-
const result = this.exec("cookies", ["get"]);
|
|
316
|
-
// Parse cookie string: "name1=value1; name2=value2" or "name1=value1\nname2=value2"
|
|
317
|
-
const cookies = [];
|
|
318
|
-
if (!result || result.trim() === "")
|
|
319
|
-
return cookies;
|
|
320
|
-
// Split by newline or semicolon
|
|
321
|
-
const parts = result.includes("\n")
|
|
322
|
-
? result.split("\n")
|
|
323
|
-
: result.split("; ");
|
|
324
|
-
for (const part of parts) {
|
|
325
|
-
const trimmed = part.trim();
|
|
326
|
-
if (!trimmed)
|
|
327
|
-
continue;
|
|
328
|
-
const eqIndex = trimmed.indexOf("=");
|
|
329
|
-
if (eqIndex > 0) {
|
|
330
|
-
cookies.push({
|
|
331
|
-
name: trimmed.substring(0, eqIndex),
|
|
332
|
-
value: trimmed.substring(eqIndex + 1),
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return cookies;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Get all localStorage
|
|
340
|
-
*/
|
|
341
|
-
async getLocalStorage() {
|
|
342
|
-
// Use single quotes to avoid shell escaping issues with parentheses
|
|
343
|
-
const script = `'(function(){var r={};for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);r[k]=localStorage.getItem(k);}return JSON.stringify(r);})()'`;
|
|
344
|
-
const result = this.exec("eval", [script]);
|
|
345
|
-
try {
|
|
346
|
-
// Result might be double-quoted string, so parse twice if needed
|
|
347
|
-
let parsed = JSON.parse(result);
|
|
348
|
-
if (typeof parsed === "string") {
|
|
349
|
-
parsed = JSON.parse(parsed);
|
|
350
|
-
}
|
|
351
|
-
return parsed || {};
|
|
352
|
-
}
|
|
353
|
-
catch {
|
|
354
|
-
return {};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
/**
|
|
358
|
-
* Get all sessionStorage
|
|
359
|
-
*/
|
|
360
|
-
async getSessionStorage() {
|
|
361
|
-
const script = `'(function(){var r={};for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i);r[k]=sessionStorage.getItem(k);}return JSON.stringify(r);})()'`;
|
|
362
|
-
const result = this.exec("eval", [script]);
|
|
363
|
-
try {
|
|
364
|
-
// Result might be double-quoted string, so parse twice if needed
|
|
365
|
-
let parsed = JSON.parse(result);
|
|
366
|
-
if (typeof parsed === "string") {
|
|
367
|
-
parsed = JSON.parse(parsed);
|
|
368
|
-
}
|
|
369
|
-
return parsed || {};
|
|
370
|
-
}
|
|
371
|
-
catch {
|
|
372
|
-
return {};
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Set localStorage value
|
|
377
|
-
*/
|
|
378
|
-
async setLocalStorage(key, value) {
|
|
379
|
-
// Escape special shell characters
|
|
380
|
-
const escapedValue = `'${value.replace(/'/g, "'\\''")}'`;
|
|
381
|
-
this.exec("storage", ["local", "set", key, escapedValue]);
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Set sessionStorage value
|
|
385
|
-
*/
|
|
386
|
-
async setSessionStorage(key, value) {
|
|
387
|
-
// Escape special shell characters
|
|
388
|
-
const escapedValue = `'${value.replace(/'/g, "'\\''")}'`;
|
|
389
|
-
this.exec("storage", ["session", "set", key, escapedValue]);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Go back
|
|
393
|
-
*/
|
|
394
|
-
async goBack() {
|
|
395
|
-
this.exec("back", []);
|
|
396
|
-
}
|
|
397
|
-
/**
|
|
398
|
-
* Go forward
|
|
399
|
-
*/
|
|
400
|
-
async goForward() {
|
|
401
|
-
this.exec("forward", []);
|
|
402
|
-
}
|
|
403
|
-
/**
|
|
404
|
-
* Reload page
|
|
405
|
-
*/
|
|
406
|
-
async reload() {
|
|
407
|
-
this.exec("reload", []);
|
|
408
|
-
}
|
|
409
|
-
/**
|
|
410
|
-
* Close the browser
|
|
411
|
-
*/
|
|
412
|
-
async close() {
|
|
413
|
-
if (this.isOpen) {
|
|
414
|
-
try {
|
|
415
|
-
this.exec("close", []);
|
|
416
|
-
}
|
|
417
|
-
catch {
|
|
418
|
-
// Ignore close errors
|
|
419
|
-
}
|
|
420
|
-
this.isOpen = false;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* Get current browser state (with graceful error handling)
|
|
425
|
-
*/
|
|
426
|
-
async getState() {
|
|
427
|
-
// Get each piece of state individually, with fallbacks
|
|
428
|
-
let url = "";
|
|
429
|
-
let title = "";
|
|
430
|
-
let snapshotData = { snapshot: "", refs: {} };
|
|
431
|
-
try {
|
|
432
|
-
url = await this.getUrl();
|
|
433
|
-
}
|
|
434
|
-
catch {
|
|
435
|
-
url = "unknown";
|
|
436
|
-
}
|
|
437
|
-
try {
|
|
438
|
-
title = await this.getTitle();
|
|
439
|
-
}
|
|
440
|
-
catch {
|
|
441
|
-
title = "";
|
|
442
|
-
}
|
|
443
|
-
try {
|
|
444
|
-
snapshotData = await this.snapshotJson();
|
|
445
|
-
}
|
|
446
|
-
catch {
|
|
447
|
-
// Try plain snapshot as fallback
|
|
448
|
-
try {
|
|
449
|
-
const plainSnapshot = await this.snapshot();
|
|
450
|
-
snapshotData = { snapshot: plainSnapshot, refs: {} };
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
snapshotData = { snapshot: "Unable to get page snapshot", refs: {} };
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return {
|
|
457
|
-
url,
|
|
458
|
-
title,
|
|
459
|
-
snapshot: snapshotData.snapshot,
|
|
460
|
-
refs: snapshotData.refs,
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Wait for page to be stable (no navigation in progress)
|
|
465
|
-
*/
|
|
466
|
-
async waitForStable(timeout = 2000) {
|
|
467
|
-
const startTime = Date.now();
|
|
468
|
-
let lastUrl = "";
|
|
469
|
-
while (Date.now() - startTime < timeout) {
|
|
470
|
-
try {
|
|
471
|
-
const currentUrl = await this.getUrl();
|
|
472
|
-
if (currentUrl === lastUrl && currentUrl !== "about:blank") {
|
|
473
|
-
// URL hasn't changed, page is likely stable
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
lastUrl = currentUrl;
|
|
477
|
-
}
|
|
478
|
-
catch {
|
|
479
|
-
// Ignore errors during stability check
|
|
480
|
-
}
|
|
481
|
-
this.sleep(200);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
//# sourceMappingURL=agent.js.map
|
|
1
|
+
import{execSync as e}from"child_process";const t=["Execution context was destroyed","Target closed","Navigation interrupted","Protocol error","Session closed","Page crashed","Frame was detached","Cannot find context"];export class BrowserAgent{config;isOpen=!1;constructor(e={}){this.config={headless:!0,timeout:3e4,viewport:{width:1280,height:720},...e}}isTransientError(e){return t.some(t=>e.toLowerCase().includes(t.toLowerCase()))}sleep(t){e("sleep "+t/1e3)}exec(t,s=[],i=2){const r=["agent-browser",t,...s].join(" "),n={...process.env};this.config.executablePath&&(n.AGENT_BROWSER_EXECUTABLE_PATH=this.config.executablePath);let a=null;for(let t=0;t<=i;t++)try{return e(r,{encoding:"utf-8",timeout:this.config.timeout,stdio:["pipe","pipe","pipe"],env:n}).trim()}catch(e){let s=e.message||e.stderr?.toString()||"";const n=s.includes("daemon already running")||s.includes("--executable-path ignored");if(e.stdout){const t=e.stdout.toString().trim();if(n&&t)return t;if(!this.isTransientError(t))return t}if(n)return"";if(this.isTransientError(s)&&t<i){this.sleep(500*(t+1));continue}a=new Error(`Browser command failed: ${r}\n${s}`)}throw a||new Error(`Browser command failed: ${r}`)}static isInstalled(){try{return e("agent-browser --version",{stdio:"pipe"}),!0}catch{return!1}}static install(){console.log("Installing agent-browser..."),e("npm install -g agent-browser",{stdio:"inherit"}),console.log("Installing browser..."),e("agent-browser install",{stdio:"inherit"})}hasInitialized=!1;async navigate(e){const t=[];if(!this.hasInitialized&&(!1===this.config.headless||this.config.executablePath)){try{this.exec("close",[]),this.sleep(1e3)}catch{}this.hasInitialized=!0}!1===this.config.headless&&t.push("--headed"),this.config.executablePath&&t.push("--executable-path",`"${this.config.executablePath}"`);const s=()=>this.exec("open",[e,...t]);s(),this.isOpen=!0;for(let e=0;e<2;e++){this.sleep(1500);try{const e=this.exec("get",["url"]).trim();if(e&&"about:blank"!==e&&"about:blank/"!==e)break;try{this.exec("close",[]),this.sleep(1e3)}catch{}s(),this.sleep(1500)}catch{break}}this.config.viewport&&this.exec("set",["viewport",String(this.config.viewport.width),String(this.config.viewport.height)])}async snapshot(e=!0){const t=e?["-i"]:[];return this.exec("snapshot",t)}async snapshotJson(){const e=this.exec("snapshot",["-i","--json"]);try{const t=JSON.parse(e);return{snapshot:t.data?.snapshot||e,refs:t.data?.refs||{}}}catch{return{snapshot:e,refs:{}}}}async click(e){this.exec("click",[e])}async fill(e,t){this.exec("fill",[e,`"${t}"`])}async type(e,t){this.exec("type",[e,`"${t}"`])}async press(e){this.exec("press",[e])}async hover(e){this.exec("hover",[e])}async select(e,t){this.exec("select",[e,t])}async scroll(e,t){const s=t?[e,String(t)]:[e];this.exec("scroll",s)}async wait(e){"number"==typeof e?this.exec("wait",[String(e)]):e.startsWith("text=")?this.exec("wait",["--text",`"${e.substring(5)}"`]):e.startsWith("url=")?this.exec("wait",["--url",`"${e.substring(4)}"`]):this.exec("wait",[e])}async screenshot(e,t=!1){const s=[];return e&&s.push(e),t&&s.push("--full"),this.exec("screenshot",s)}async getText(e){return this.exec("get",["text",e])}async getUrl(){return this.exec("get",["url"])}async getTitle(){return this.exec("get",["title"])}async isVisible(e){return this.exec("is",["visible",e]).toLowerCase().includes("true")}async evaluate(e){return this.exec("eval",[`"${e.replace(/"/g,'\\"')}"`])}async setCookie(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("cookies",["set",e,s])}async getCookies(){const e=this.exec("cookies",["get"]),t=[];if(!e||""===e.trim())return t;const s=e.includes("\n")?e.split("\n"):e.split("; ");for(const e of s){const s=e.trim();if(!s)continue;const i=s.indexOf("=");i>0&&t.push({name:s.substring(0,i),value:s.substring(i+1)})}return t}async getLocalStorage(){const e=this.exec("eval",["'(function(){var r={};for(var i=0;i<localStorage.length;i++){var k=localStorage.key(i);r[k]=localStorage.getItem(k);}return JSON.stringify(r);})()'"]);try{let t=JSON.parse(e);return"string"==typeof t&&(t=JSON.parse(t)),t||{}}catch{return{}}}async getSessionStorage(){const e=this.exec("eval",["'(function(){var r={};for(var i=0;i<sessionStorage.length;i++){var k=sessionStorage.key(i);r[k]=sessionStorage.getItem(k);}return JSON.stringify(r);})()'"]);try{let t=JSON.parse(e);return"string"==typeof t&&(t=JSON.parse(t)),t||{}}catch{return{}}}async setLocalStorage(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("storage",["local","set",e,s])}async setSessionStorage(e,t){const s=`'${t.replace(/'/g,"'\\''")}'`;this.exec("storage",["session","set",e,s])}async goBack(){this.exec("back",[])}async goForward(){this.exec("forward",[])}async reload(){this.exec("reload",[])}async close(){if(this.isOpen){try{this.exec("close",[])}catch{}this.isOpen=!1}}async getState(){let e="",t="",s={snapshot:"",refs:{}};try{e=await this.getUrl()}catch{e="unknown"}try{t=await this.getTitle()}catch{t=""}try{s=await this.snapshotJson()}catch{try{s={snapshot:await this.snapshot(),refs:{}}}catch{s={snapshot:"Unable to get page snapshot",refs:{}}}}return{url:e,title:t,snapshot:s.snapshot,refs:s.refs}}async waitForStable(e=2e3){const t=Date.now();let s="";for(;Date.now()-t<e;){try{const e=await this.getUrl();if(e===s&&"about:blank"!==e)return;s=e}catch{}this.sleep(200)}}}
|