@hasna/browser 0.0.2 → 0.0.4
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/cli/index.js +1576 -289
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/index.js +418 -157
- package/dist/lib/actions-ref.test.d.ts +2 -0
- package/dist/lib/actions-ref.test.d.ts.map +1 -0
- package/dist/lib/actions.d.ts +12 -0
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/annotate.d.ts +18 -0
- package/dist/lib/annotate.d.ts.map +1 -0
- package/dist/lib/annotate.test.d.ts +2 -0
- package/dist/lib/annotate.test.d.ts.map +1 -0
- package/dist/lib/dialogs.d.ts +15 -0
- package/dist/lib/dialogs.d.ts.map +1 -0
- package/dist/lib/profiles.d.ts +23 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- package/dist/lib/screenshot-v4.test.d.ts +2 -0
- package/dist/lib/screenshot-v4.test.d.ts.map +1 -0
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/session-v3.test.d.ts +2 -0
- package/dist/lib/session-v3.test.d.ts.map +1 -0
- package/dist/lib/session.d.ts +5 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot-diff.test.d.ts +2 -0
- package/dist/lib/snapshot-diff.test.d.ts.map +1 -0
- package/dist/lib/snapshot.d.ts +33 -0
- package/dist/lib/snapshot.d.ts.map +1 -0
- package/dist/lib/snapshot.test.d.ts +2 -0
- package/dist/lib/snapshot.test.d.ts.map +1 -0
- package/dist/lib/stealth.d.ts +5 -0
- package/dist/lib/stealth.d.ts.map +1 -0
- package/dist/lib/stealth.test.d.ts +2 -0
- package/dist/lib/stealth.test.d.ts.map +1 -0
- package/dist/lib/tabs.d.ts +18 -0
- package/dist/lib/tabs.d.ts.map +1 -0
- package/dist/mcp/index.js +1591 -312
- package/dist/mcp/v4.test.d.ts +2 -0
- package/dist/mcp/v4.test.d.ts.map +1 -0
- package/dist/server/index.js +340 -173
- package/dist/types/index.d.ts +35 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -30,6 +30,12 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
30
30
|
var __require = import.meta.require;
|
|
31
31
|
|
|
32
32
|
// src/db/schema.ts
|
|
33
|
+
var exports_schema = {};
|
|
34
|
+
__export(exports_schema, {
|
|
35
|
+
resetDatabase: () => resetDatabase,
|
|
36
|
+
getDatabase: () => getDatabase,
|
|
37
|
+
getDataDir: () => getDataDir
|
|
38
|
+
});
|
|
33
39
|
import { Database } from "bun:sqlite";
|
|
34
40
|
import { join } from "path";
|
|
35
41
|
import { mkdirSync } from "fs";
|
|
@@ -55,6 +61,15 @@ function getDatabase(path) {
|
|
|
55
61
|
runMigrations(_db);
|
|
56
62
|
return _db;
|
|
57
63
|
}
|
|
64
|
+
function resetDatabase() {
|
|
65
|
+
if (_db) {
|
|
66
|
+
try {
|
|
67
|
+
_db.close();
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
_db = null;
|
|
71
|
+
_dbPath = null;
|
|
72
|
+
}
|
|
58
73
|
function runMigrations(db) {
|
|
59
74
|
db.exec(`
|
|
60
75
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
@@ -217,6 +232,244 @@ function runMigrations(db) {
|
|
|
217
232
|
var _db = null, _dbPath = null;
|
|
218
233
|
var init_schema = () => {};
|
|
219
234
|
|
|
235
|
+
// src/db/console-log.ts
|
|
236
|
+
var exports_console_log = {};
|
|
237
|
+
__export(exports_console_log, {
|
|
238
|
+
logConsoleMessage: () => logConsoleMessage,
|
|
239
|
+
getConsoleMessage: () => getConsoleMessage,
|
|
240
|
+
getConsoleLog: () => getConsoleLog,
|
|
241
|
+
clearConsoleLog: () => clearConsoleLog
|
|
242
|
+
});
|
|
243
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
244
|
+
function logConsoleMessage(data) {
|
|
245
|
+
const db = getDatabase();
|
|
246
|
+
const id = randomUUID3();
|
|
247
|
+
db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
|
|
248
|
+
return getConsoleMessage(id);
|
|
249
|
+
}
|
|
250
|
+
function getConsoleMessage(id) {
|
|
251
|
+
const db = getDatabase();
|
|
252
|
+
return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
|
|
253
|
+
}
|
|
254
|
+
function getConsoleLog(sessionId, level) {
|
|
255
|
+
const db = getDatabase();
|
|
256
|
+
if (level) {
|
|
257
|
+
return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
|
|
258
|
+
}
|
|
259
|
+
return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
260
|
+
}
|
|
261
|
+
function clearConsoleLog(sessionId) {
|
|
262
|
+
const db = getDatabase();
|
|
263
|
+
db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
|
|
264
|
+
}
|
|
265
|
+
var init_console_log = __esm(() => {
|
|
266
|
+
init_schema();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// src/lib/snapshot.ts
|
|
270
|
+
var exports_snapshot = {};
|
|
271
|
+
__export(exports_snapshot, {
|
|
272
|
+
takeSnapshot: () => takeSnapshot,
|
|
273
|
+
setLastSnapshot: () => setLastSnapshot,
|
|
274
|
+
hasRefs: () => hasRefs,
|
|
275
|
+
getSessionRefs: () => getSessionRefs,
|
|
276
|
+
getRefLocator: () => getRefLocator,
|
|
277
|
+
getRefInfo: () => getRefInfo,
|
|
278
|
+
getLastSnapshot: () => getLastSnapshot,
|
|
279
|
+
diffSnapshots: () => diffSnapshots,
|
|
280
|
+
clearSessionRefs: () => clearSessionRefs,
|
|
281
|
+
clearLastSnapshot: () => clearLastSnapshot
|
|
282
|
+
});
|
|
283
|
+
function getLastSnapshot(sessionId) {
|
|
284
|
+
return lastSnapshots.get(sessionId) ?? null;
|
|
285
|
+
}
|
|
286
|
+
function setLastSnapshot(sessionId, snapshot) {
|
|
287
|
+
lastSnapshots.set(sessionId, snapshot);
|
|
288
|
+
}
|
|
289
|
+
function clearLastSnapshot(sessionId) {
|
|
290
|
+
lastSnapshots.delete(sessionId);
|
|
291
|
+
}
|
|
292
|
+
async function takeSnapshot(page, sessionId) {
|
|
293
|
+
let ariaTree;
|
|
294
|
+
try {
|
|
295
|
+
ariaTree = await page.locator("body").ariaSnapshot();
|
|
296
|
+
} catch {
|
|
297
|
+
ariaTree = "";
|
|
298
|
+
}
|
|
299
|
+
const refs = {};
|
|
300
|
+
const refMap = new Map;
|
|
301
|
+
let refCounter = 0;
|
|
302
|
+
for (const role of INTERACTIVE_ROLES) {
|
|
303
|
+
const locators = page.getByRole(role);
|
|
304
|
+
const count = await locators.count();
|
|
305
|
+
for (let i = 0;i < count; i++) {
|
|
306
|
+
const el = locators.nth(i);
|
|
307
|
+
let name = "";
|
|
308
|
+
let visible = false;
|
|
309
|
+
let enabled = true;
|
|
310
|
+
let value;
|
|
311
|
+
let checked;
|
|
312
|
+
try {
|
|
313
|
+
visible = await el.isVisible();
|
|
314
|
+
if (!visible)
|
|
315
|
+
continue;
|
|
316
|
+
} catch {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
name = await el.evaluate((e) => {
|
|
321
|
+
const el2 = e;
|
|
322
|
+
return el2.getAttribute("aria-label") ?? el2.textContent?.trim().slice(0, 80) ?? el2.getAttribute("title") ?? el2.getAttribute("placeholder") ?? "";
|
|
323
|
+
});
|
|
324
|
+
} catch {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (!name)
|
|
328
|
+
continue;
|
|
329
|
+
try {
|
|
330
|
+
enabled = await el.isEnabled();
|
|
331
|
+
} catch {}
|
|
332
|
+
try {
|
|
333
|
+
if (role === "checkbox" || role === "radio" || role === "switch") {
|
|
334
|
+
checked = await el.isChecked();
|
|
335
|
+
}
|
|
336
|
+
} catch {}
|
|
337
|
+
try {
|
|
338
|
+
if (role === "textbox" || role === "searchbox" || role === "spinbutton" || role === "combobox") {
|
|
339
|
+
value = await el.inputValue();
|
|
340
|
+
}
|
|
341
|
+
} catch {}
|
|
342
|
+
const ref = `@e${refCounter}`;
|
|
343
|
+
refCounter++;
|
|
344
|
+
refs[ref] = { role, name, visible, enabled, value, checked };
|
|
345
|
+
const escapedName = name.replace(/"/g, "\\\"");
|
|
346
|
+
refMap.set(ref, { role, name, locatorSelector: `role=${role}[name="${escapedName}"]` });
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
let annotatedTree = ariaTree;
|
|
350
|
+
for (const [ref, info] of Object.entries(refs)) {
|
|
351
|
+
const escapedName = info.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
352
|
+
const pattern = new RegExp(`(${info.role}\\s+"${escapedName.slice(0, 40)}[^"]*")`, "m");
|
|
353
|
+
const match = annotatedTree.match(pattern);
|
|
354
|
+
if (match) {
|
|
355
|
+
annotatedTree = annotatedTree.replace(match[0], `${match[0]} [${ref}]`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const unmatchedRefs = Object.entries(refs).filter(([ref]) => !annotatedTree.includes(`[${ref}]`));
|
|
359
|
+
if (unmatchedRefs.length > 0) {
|
|
360
|
+
annotatedTree += `
|
|
361
|
+
|
|
362
|
+
--- Interactive elements ---`;
|
|
363
|
+
for (const [ref, info] of unmatchedRefs) {
|
|
364
|
+
const extras = [];
|
|
365
|
+
if (info.checked !== undefined)
|
|
366
|
+
extras.push(`checked=${info.checked}`);
|
|
367
|
+
if (!info.enabled)
|
|
368
|
+
extras.push("disabled");
|
|
369
|
+
if (info.value)
|
|
370
|
+
extras.push(`value="${info.value}"`);
|
|
371
|
+
const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
|
|
372
|
+
annotatedTree += `
|
|
373
|
+
${info.role} "${info.name}" [${ref}]${extrasStr}`;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (sessionId) {
|
|
377
|
+
sessionRefMaps.set(sessionId, refMap);
|
|
378
|
+
}
|
|
379
|
+
return {
|
|
380
|
+
tree: annotatedTree,
|
|
381
|
+
refs,
|
|
382
|
+
interactive_count: refCounter
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function getRefLocator(page, sessionId, ref) {
|
|
386
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
387
|
+
if (!refMap)
|
|
388
|
+
throw new Error(`No snapshot taken for session ${sessionId}. Call browser_snapshot first.`);
|
|
389
|
+
const entry = refMap.get(ref);
|
|
390
|
+
if (!entry)
|
|
391
|
+
throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
|
|
392
|
+
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
393
|
+
}
|
|
394
|
+
function getRefInfo(sessionId, ref) {
|
|
395
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
396
|
+
if (!refMap)
|
|
397
|
+
return null;
|
|
398
|
+
return refMap.get(ref) ?? null;
|
|
399
|
+
}
|
|
400
|
+
function getSessionRefs(sessionId) {
|
|
401
|
+
return sessionRefMaps.get(sessionId) ?? null;
|
|
402
|
+
}
|
|
403
|
+
function clearSessionRefs(sessionId) {
|
|
404
|
+
sessionRefMaps.delete(sessionId);
|
|
405
|
+
}
|
|
406
|
+
function hasRefs(sessionId) {
|
|
407
|
+
return sessionRefMaps.has(sessionId) && (sessionRefMaps.get(sessionId)?.size ?? 0) > 0;
|
|
408
|
+
}
|
|
409
|
+
function refKey(info) {
|
|
410
|
+
return `${info.role}::${info.name}`;
|
|
411
|
+
}
|
|
412
|
+
function diffSnapshots(before, after) {
|
|
413
|
+
const beforeMap = new Map;
|
|
414
|
+
for (const [ref, info] of Object.entries(before.refs)) {
|
|
415
|
+
beforeMap.set(refKey(info), { ref, info });
|
|
416
|
+
}
|
|
417
|
+
const afterMap = new Map;
|
|
418
|
+
for (const [ref, info] of Object.entries(after.refs)) {
|
|
419
|
+
afterMap.set(refKey(info), { ref, info });
|
|
420
|
+
}
|
|
421
|
+
const added = [];
|
|
422
|
+
const removed = [];
|
|
423
|
+
const modified = [];
|
|
424
|
+
for (const [key, afterEntry] of afterMap) {
|
|
425
|
+
const beforeEntry = beforeMap.get(key);
|
|
426
|
+
if (!beforeEntry) {
|
|
427
|
+
added.push({ ref: afterEntry.ref, info: afterEntry.info });
|
|
428
|
+
} else {
|
|
429
|
+
const b = beforeEntry.info;
|
|
430
|
+
const a = afterEntry.info;
|
|
431
|
+
if (b.visible !== a.visible || b.enabled !== a.enabled || b.value !== a.value || b.checked !== a.checked || b.description !== a.description) {
|
|
432
|
+
modified.push({ ref: afterEntry.ref, before: b, after: a });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
for (const [key, beforeEntry] of beforeMap) {
|
|
437
|
+
if (!afterMap.has(key)) {
|
|
438
|
+
removed.push({ ref: beforeEntry.ref, info: beforeEntry.info });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const url_changed = before.tree.split(`
|
|
442
|
+
`)[0] !== after.tree.split(`
|
|
443
|
+
`)[0];
|
|
444
|
+
const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
|
|
445
|
+
return { added, removed, modified, url_changed, title_changed };
|
|
446
|
+
}
|
|
447
|
+
var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
|
|
448
|
+
var init_snapshot = __esm(() => {
|
|
449
|
+
lastSnapshots = new Map;
|
|
450
|
+
sessionRefMaps = new Map;
|
|
451
|
+
INTERACTIVE_ROLES = [
|
|
452
|
+
"button",
|
|
453
|
+
"link",
|
|
454
|
+
"textbox",
|
|
455
|
+
"checkbox",
|
|
456
|
+
"radio",
|
|
457
|
+
"combobox",
|
|
458
|
+
"menuitem",
|
|
459
|
+
"menuitemcheckbox",
|
|
460
|
+
"menuitemradio",
|
|
461
|
+
"option",
|
|
462
|
+
"searchbox",
|
|
463
|
+
"slider",
|
|
464
|
+
"spinbutton",
|
|
465
|
+
"switch",
|
|
466
|
+
"tab",
|
|
467
|
+
"treeitem",
|
|
468
|
+
"listbox",
|
|
469
|
+
"menu"
|
|
470
|
+
];
|
|
471
|
+
});
|
|
472
|
+
|
|
220
473
|
// node_modules/sharp/lib/is.js
|
|
221
474
|
var require_is = __commonJS((exports, module) => {
|
|
222
475
|
/*!
|
|
@@ -6612,38 +6865,64 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
6612
6865
|
module.exports = Sharp;
|
|
6613
6866
|
});
|
|
6614
6867
|
|
|
6615
|
-
// src/
|
|
6616
|
-
var
|
|
6617
|
-
__export(
|
|
6618
|
-
|
|
6619
|
-
getConsoleMessage: () => getConsoleMessage,
|
|
6620
|
-
getConsoleLog: () => getConsoleLog,
|
|
6621
|
-
clearConsoleLog: () => clearConsoleLog
|
|
6868
|
+
// src/lib/annotate.ts
|
|
6869
|
+
var exports_annotate = {};
|
|
6870
|
+
__export(exports_annotate, {
|
|
6871
|
+
annotateScreenshot: () => annotateScreenshot
|
|
6622
6872
|
});
|
|
6623
|
-
|
|
6624
|
-
|
|
6625
|
-
const
|
|
6626
|
-
const
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6873
|
+
async function annotateScreenshot(page, sessionId) {
|
|
6874
|
+
const snapshot = await takeSnapshot(page, sessionId);
|
|
6875
|
+
const rawBuffer = await page.screenshot({ type: "png" });
|
|
6876
|
+
const meta = await import_sharp3.default(rawBuffer).metadata();
|
|
6877
|
+
const imgWidth = meta.width ?? 1280;
|
|
6878
|
+
const imgHeight = meta.height ?? 720;
|
|
6879
|
+
const annotations = [];
|
|
6880
|
+
const labelToRef = {};
|
|
6881
|
+
let labelCounter = 1;
|
|
6882
|
+
const refsToAnnotate = Object.entries(snapshot.refs).slice(0, MAX_ANNOTATIONS);
|
|
6883
|
+
for (const [ref, info] of refsToAnnotate) {
|
|
6884
|
+
try {
|
|
6885
|
+
const locator = page.getByRole(info.role, { name: info.name }).first();
|
|
6886
|
+
const box = await locator.boundingBox();
|
|
6887
|
+
if (!box)
|
|
6888
|
+
continue;
|
|
6889
|
+
const annotation = {
|
|
6890
|
+
ref,
|
|
6891
|
+
label: labelCounter,
|
|
6892
|
+
x: Math.round(box.x),
|
|
6893
|
+
y: Math.round(box.y),
|
|
6894
|
+
width: Math.round(box.width),
|
|
6895
|
+
height: Math.round(box.height),
|
|
6896
|
+
role: info.role,
|
|
6897
|
+
name: info.name
|
|
6898
|
+
};
|
|
6899
|
+
annotations.push(annotation);
|
|
6900
|
+
labelToRef[labelCounter] = ref;
|
|
6901
|
+
labelCounter++;
|
|
6902
|
+
} catch {}
|
|
6638
6903
|
}
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
const
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6904
|
+
const circleR = 10;
|
|
6905
|
+
const fontSize = 12;
|
|
6906
|
+
const svgParts = [];
|
|
6907
|
+
for (const ann of annotations) {
|
|
6908
|
+
const cx = Math.min(Math.max(ann.x + circleR, circleR), imgWidth - circleR);
|
|
6909
|
+
const cy = Math.min(Math.max(ann.y - circleR - 2, circleR), imgHeight - circleR);
|
|
6910
|
+
svgParts.push(`
|
|
6911
|
+
<circle cx="${cx}" cy="${cy}" r="${circleR}" fill="#e11d48" stroke="white" stroke-width="1.5"/>
|
|
6912
|
+
<text x="${cx}" y="${cy + 4}" text-anchor="middle" fill="white" font-size="${fontSize}" font-family="Arial,sans-serif" font-weight="bold">${ann.label}</text>
|
|
6913
|
+
`);
|
|
6914
|
+
svgParts.push(`
|
|
6915
|
+
<rect x="${ann.x}" y="${ann.y}" width="${ann.width}" height="${ann.height}" fill="none" stroke="#e11d48" stroke-width="1.5" stroke-opacity="0.6" rx="2"/>
|
|
6916
|
+
`);
|
|
6917
|
+
}
|
|
6918
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">${svgParts.join("")}</svg>`;
|
|
6919
|
+
const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
|
|
6920
|
+
return { buffer: annotatedBuffer, annotations, labelToRef };
|
|
6921
|
+
}
|
|
6922
|
+
var import_sharp3, MAX_ANNOTATIONS = 40;
|
|
6923
|
+
var init_annotate = __esm(() => {
|
|
6924
|
+
init_snapshot();
|
|
6925
|
+
import_sharp3 = __toESM(require_lib(), 1);
|
|
6647
6926
|
});
|
|
6648
6927
|
|
|
6649
6928
|
// src/mcp/index.ts
|
|
@@ -10623,6 +10902,10 @@ var coerce = {
|
|
|
10623
10902
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10624
10903
|
};
|
|
10625
10904
|
var NEVER = INVALID;
|
|
10905
|
+
// src/mcp/index.ts
|
|
10906
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
10907
|
+
import { join as join7 } from "path";
|
|
10908
|
+
|
|
10626
10909
|
// src/types/index.ts
|
|
10627
10910
|
class BrowserError extends Error {
|
|
10628
10911
|
code;
|
|
@@ -10690,7 +10973,14 @@ import { randomUUID } from "crypto";
|
|
|
10690
10973
|
function createSession(data) {
|
|
10691
10974
|
const db = getDatabase();
|
|
10692
10975
|
const id = randomUUID();
|
|
10693
|
-
|
|
10976
|
+
let name = data.name ?? null;
|
|
10977
|
+
if (name) {
|
|
10978
|
+
const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
|
|
10979
|
+
if (existing) {
|
|
10980
|
+
name = `${name}-${id.slice(0, 6)}`;
|
|
10981
|
+
}
|
|
10982
|
+
}
|
|
10983
|
+
db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, name);
|
|
10694
10984
|
return getSession(id);
|
|
10695
10985
|
}
|
|
10696
10986
|
function getSessionByName(name) {
|
|
@@ -10887,83 +11177,418 @@ function selectEngine(useCase, explicit) {
|
|
|
10887
11177
|
return preferred;
|
|
10888
11178
|
}
|
|
10889
11179
|
|
|
10890
|
-
// src/
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
const
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
page = await context.newPage();
|
|
10901
|
-
} else {
|
|
10902
|
-
browser = await launchPlaywright({
|
|
10903
|
-
headless: opts.headless ?? true,
|
|
10904
|
-
viewport: opts.viewport,
|
|
10905
|
-
userAgent: opts.userAgent
|
|
10906
|
-
});
|
|
10907
|
-
page = await getPage(browser, {
|
|
10908
|
-
viewport: opts.viewport,
|
|
10909
|
-
userAgent: opts.userAgent
|
|
10910
|
-
});
|
|
10911
|
-
}
|
|
10912
|
-
const session = createSession({
|
|
10913
|
-
engine: resolvedEngine,
|
|
10914
|
-
projectId: opts.projectId,
|
|
10915
|
-
agentId: opts.agentId,
|
|
10916
|
-
startUrl: opts.startUrl
|
|
10917
|
-
});
|
|
10918
|
-
handles.set(session.id, { browser, page, engine: resolvedEngine });
|
|
10919
|
-
if (opts.startUrl) {
|
|
10920
|
-
try {
|
|
10921
|
-
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
10922
|
-
} catch (err) {}
|
|
10923
|
-
}
|
|
10924
|
-
return { session, page };
|
|
11180
|
+
// src/db/network-log.ts
|
|
11181
|
+
init_schema();
|
|
11182
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
11183
|
+
function logRequest(data) {
|
|
11184
|
+
const db = getDatabase();
|
|
11185
|
+
const id = randomUUID2();
|
|
11186
|
+
db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
|
|
11187
|
+
response_headers, request_body, body_size, duration_ms, resource_type)
|
|
11188
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
|
|
11189
|
+
return getNetworkRequest(id);
|
|
10925
11190
|
}
|
|
10926
|
-
function
|
|
10927
|
-
const
|
|
10928
|
-
|
|
10929
|
-
throw new SessionNotFoundError(sessionId);
|
|
10930
|
-
return handle.page;
|
|
11191
|
+
function getNetworkRequest(id) {
|
|
11192
|
+
const db = getDatabase();
|
|
11193
|
+
return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
|
|
10931
11194
|
}
|
|
10932
|
-
|
|
10933
|
-
const
|
|
10934
|
-
|
|
10935
|
-
|
|
10936
|
-
|
|
10937
|
-
|
|
11195
|
+
function getNetworkLog(sessionId) {
|
|
11196
|
+
const db = getDatabase();
|
|
11197
|
+
return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
11198
|
+
}
|
|
11199
|
+
|
|
11200
|
+
// src/lib/network.ts
|
|
11201
|
+
function enableNetworkLogging(page, sessionId) {
|
|
11202
|
+
const requestStart = new Map;
|
|
11203
|
+
const onRequest = (req) => {
|
|
11204
|
+
requestStart.set(req.url(), Date.now());
|
|
11205
|
+
};
|
|
11206
|
+
const onResponse = (res) => {
|
|
11207
|
+
const start = requestStart.get(res.url()) ?? Date.now();
|
|
11208
|
+
const duration = Date.now() - start;
|
|
11209
|
+
const req = res.request();
|
|
10938
11210
|
try {
|
|
10939
|
-
|
|
11211
|
+
logRequest({
|
|
11212
|
+
session_id: sessionId,
|
|
11213
|
+
method: req.method(),
|
|
11214
|
+
url: res.url(),
|
|
11215
|
+
status_code: res.status(),
|
|
11216
|
+
request_headers: JSON.stringify(req.headers()),
|
|
11217
|
+
response_headers: JSON.stringify(res.headers()),
|
|
11218
|
+
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
11219
|
+
duration_ms: duration,
|
|
11220
|
+
resource_type: req.resourceType()
|
|
11221
|
+
});
|
|
10940
11222
|
} catch {}
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
|
|
10946
|
-
|
|
10947
|
-
}
|
|
10948
|
-
function getSessionByName2(name) {
|
|
10949
|
-
return getSessionByName(name);
|
|
10950
|
-
}
|
|
10951
|
-
function renameSession2(id, name) {
|
|
10952
|
-
return renameSession(id, name);
|
|
11223
|
+
};
|
|
11224
|
+
page.on("request", onRequest);
|
|
11225
|
+
page.on("response", onResponse);
|
|
11226
|
+
return () => {
|
|
11227
|
+
page.off("request", onRequest);
|
|
11228
|
+
page.off("response", onResponse);
|
|
11229
|
+
};
|
|
10953
11230
|
}
|
|
10954
|
-
|
|
10955
|
-
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10961
|
-
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
11231
|
+
async function addInterceptRule(page, rule) {
|
|
11232
|
+
await page.route(rule.pattern, async (route) => {
|
|
11233
|
+
if (rule.action === "block") {
|
|
11234
|
+
await route.abort();
|
|
11235
|
+
} else if (rule.action === "modify" && rule.response) {
|
|
11236
|
+
await route.fulfill({
|
|
11237
|
+
status: rule.response.status,
|
|
11238
|
+
body: rule.response.body,
|
|
11239
|
+
headers: rule.response.headers
|
|
11240
|
+
});
|
|
11241
|
+
} else {
|
|
11242
|
+
await route.continue();
|
|
11243
|
+
}
|
|
11244
|
+
});
|
|
11245
|
+
}
|
|
11246
|
+
function startHAR(page) {
|
|
11247
|
+
const entries = [];
|
|
11248
|
+
const requestStart = new Map;
|
|
11249
|
+
const onRequest = (req) => {
|
|
11250
|
+
requestStart.set(req.url() + req.method(), {
|
|
11251
|
+
time: Date.now(),
|
|
11252
|
+
method: req.method(),
|
|
11253
|
+
headers: req.headers(),
|
|
11254
|
+
postData: req.postData() ?? undefined
|
|
11255
|
+
});
|
|
11256
|
+
};
|
|
11257
|
+
const onResponse = async (res) => {
|
|
11258
|
+
const key = res.url() + res.request().method();
|
|
11259
|
+
const start = requestStart.get(key);
|
|
11260
|
+
if (!start)
|
|
11261
|
+
return;
|
|
11262
|
+
const duration = Date.now() - start.time;
|
|
11263
|
+
const entry = {
|
|
11264
|
+
startedDateTime: new Date(start.time).toISOString(),
|
|
11265
|
+
time: duration,
|
|
11266
|
+
request: {
|
|
11267
|
+
method: start.method,
|
|
11268
|
+
url: res.url(),
|
|
11269
|
+
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
11270
|
+
postData: start.postData ? { text: start.postData } : undefined
|
|
11271
|
+
},
|
|
11272
|
+
response: {
|
|
11273
|
+
status: res.status(),
|
|
11274
|
+
statusText: res.statusText(),
|
|
11275
|
+
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
11276
|
+
content: {
|
|
11277
|
+
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
11278
|
+
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
11279
|
+
}
|
|
11280
|
+
},
|
|
11281
|
+
timings: { send: 0, wait: duration, receive: 0 }
|
|
11282
|
+
};
|
|
11283
|
+
entries.push(entry);
|
|
11284
|
+
requestStart.delete(key);
|
|
11285
|
+
};
|
|
11286
|
+
page.on("request", onRequest);
|
|
11287
|
+
page.on("response", onResponse);
|
|
11288
|
+
return {
|
|
11289
|
+
entries,
|
|
11290
|
+
stop: () => {
|
|
11291
|
+
page.off("request", onRequest);
|
|
11292
|
+
page.off("response", onResponse);
|
|
11293
|
+
return {
|
|
11294
|
+
log: {
|
|
11295
|
+
version: "1.2",
|
|
11296
|
+
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
11297
|
+
entries
|
|
11298
|
+
}
|
|
11299
|
+
};
|
|
11300
|
+
}
|
|
11301
|
+
};
|
|
11302
|
+
}
|
|
11303
|
+
|
|
11304
|
+
// src/lib/console.ts
|
|
11305
|
+
init_console_log();
|
|
11306
|
+
function enableConsoleCapture(page, sessionId) {
|
|
11307
|
+
const onConsole = (msg) => {
|
|
11308
|
+
const levelMap = {
|
|
11309
|
+
log: "log",
|
|
11310
|
+
warn: "warn",
|
|
11311
|
+
error: "error",
|
|
11312
|
+
debug: "debug",
|
|
11313
|
+
info: "info",
|
|
11314
|
+
warning: "warn"
|
|
11315
|
+
};
|
|
11316
|
+
const level = levelMap[msg.type()] ?? "log";
|
|
11317
|
+
const location = msg.location();
|
|
11318
|
+
try {
|
|
11319
|
+
logConsoleMessage({
|
|
11320
|
+
session_id: sessionId,
|
|
11321
|
+
level,
|
|
11322
|
+
message: msg.text(),
|
|
11323
|
+
source: location.url || undefined,
|
|
11324
|
+
line_number: location.lineNumber || undefined
|
|
11325
|
+
});
|
|
11326
|
+
} catch {}
|
|
11327
|
+
};
|
|
11328
|
+
page.on("console", onConsole);
|
|
11329
|
+
return () => page.off("console", onConsole);
|
|
11330
|
+
}
|
|
11331
|
+
|
|
11332
|
+
// src/lib/stealth.ts
|
|
11333
|
+
var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
|
|
11334
|
+
var STEALTH_SCRIPT = `
|
|
11335
|
+
// \u2500\u2500 1. Remove navigator.webdriver flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11336
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
11337
|
+
get: () => false,
|
|
11338
|
+
configurable: true,
|
|
11339
|
+
});
|
|
11340
|
+
|
|
11341
|
+
// \u2500\u2500 2. Override navigator.plugins to show typical Chrome plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11342
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
11343
|
+
get: () => {
|
|
11344
|
+
const plugins = [
|
|
11345
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
11346
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
11347
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
|
|
11348
|
+
];
|
|
11349
|
+
// Mimic PluginArray interface
|
|
11350
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
11351
|
+
plugins.forEach((p, i) => {
|
|
11352
|
+
const plugin = Object.create(Plugin.prototype);
|
|
11353
|
+
Object.defineProperties(plugin, {
|
|
11354
|
+
name: { value: p.name, enumerable: true },
|
|
11355
|
+
filename: { value: p.filename, enumerable: true },
|
|
11356
|
+
description: { value: p.description, enumerable: true },
|
|
11357
|
+
length: { value: p.length, enumerable: true },
|
|
11358
|
+
});
|
|
11359
|
+
pluginArray[i] = plugin;
|
|
11360
|
+
});
|
|
11361
|
+
Object.defineProperty(pluginArray, 'length', { value: plugins.length });
|
|
11362
|
+
pluginArray.item = (i) => pluginArray[i] || null;
|
|
11363
|
+
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
|
|
11364
|
+
pluginArray.refresh = () => {};
|
|
11365
|
+
return pluginArray;
|
|
11366
|
+
},
|
|
11367
|
+
configurable: true,
|
|
11368
|
+
});
|
|
11369
|
+
|
|
11370
|
+
// \u2500\u2500 3. Override navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11371
|
+
Object.defineProperty(navigator, 'languages', {
|
|
11372
|
+
get: () => ['en-US', 'en'],
|
|
11373
|
+
configurable: true,
|
|
11374
|
+
});
|
|
11375
|
+
|
|
11376
|
+
// \u2500\u2500 4. Override chrome.runtime to appear like real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
11377
|
+
if (!window.chrome) {
|
|
11378
|
+
window.chrome = {};
|
|
11379
|
+
}
|
|
11380
|
+
if (!window.chrome.runtime) {
|
|
11381
|
+
window.chrome.runtime = {
|
|
11382
|
+
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {} }; },
|
|
11383
|
+
sendMessage: function() {},
|
|
11384
|
+
onMessage: { addListener: function() {}, removeListener: function() {} },
|
|
11385
|
+
id: undefined,
|
|
11386
|
+
};
|
|
11387
|
+
}
|
|
11388
|
+
`;
|
|
11389
|
+
async function applyStealthPatches(page) {
|
|
11390
|
+
await page.context().addInitScript(STEALTH_SCRIPT);
|
|
11391
|
+
await page.context().setExtraHTTPHeaders({
|
|
11392
|
+
"User-Agent": REALISTIC_USER_AGENT
|
|
11393
|
+
});
|
|
11394
|
+
}
|
|
11395
|
+
|
|
11396
|
+
// src/lib/dialogs.ts
|
|
11397
|
+
var pendingDialogs = new Map;
|
|
11398
|
+
var AUTO_DISMISS_MS = 5000;
|
|
11399
|
+
function setupDialogHandler(page, sessionId) {
|
|
11400
|
+
const onDialog = (dialog) => {
|
|
11401
|
+
const info = {
|
|
11402
|
+
type: dialog.type(),
|
|
11403
|
+
message: dialog.message(),
|
|
11404
|
+
default_value: dialog.defaultValue(),
|
|
11405
|
+
timestamp: new Date().toISOString()
|
|
11406
|
+
};
|
|
11407
|
+
const autoTimer = setTimeout(() => {
|
|
11408
|
+
try {
|
|
11409
|
+
dialog.dismiss().catch(() => {});
|
|
11410
|
+
} catch {}
|
|
11411
|
+
const list = pendingDialogs.get(sessionId);
|
|
11412
|
+
if (list) {
|
|
11413
|
+
const idx = list.findIndex((p) => p.dialog === dialog);
|
|
11414
|
+
if (idx >= 0)
|
|
11415
|
+
list.splice(idx, 1);
|
|
11416
|
+
if (list.length === 0)
|
|
11417
|
+
pendingDialogs.delete(sessionId);
|
|
11418
|
+
}
|
|
11419
|
+
}, AUTO_DISMISS_MS);
|
|
11420
|
+
const pending = { dialog, info, autoTimer };
|
|
11421
|
+
if (!pendingDialogs.has(sessionId)) {
|
|
11422
|
+
pendingDialogs.set(sessionId, []);
|
|
11423
|
+
}
|
|
11424
|
+
pendingDialogs.get(sessionId).push(pending);
|
|
11425
|
+
};
|
|
11426
|
+
page.on("dialog", onDialog);
|
|
11427
|
+
return () => {
|
|
11428
|
+
page.off("dialog", onDialog);
|
|
11429
|
+
const list = pendingDialogs.get(sessionId);
|
|
11430
|
+
if (list) {
|
|
11431
|
+
for (const p of list)
|
|
11432
|
+
clearTimeout(p.autoTimer);
|
|
11433
|
+
pendingDialogs.delete(sessionId);
|
|
11434
|
+
}
|
|
11435
|
+
};
|
|
11436
|
+
}
|
|
11437
|
+
function getDialogs(sessionId) {
|
|
11438
|
+
const list = pendingDialogs.get(sessionId);
|
|
11439
|
+
if (!list)
|
|
11440
|
+
return [];
|
|
11441
|
+
return list.map((p) => p.info);
|
|
11442
|
+
}
|
|
11443
|
+
async function handleDialog(sessionId, action, promptText) {
|
|
11444
|
+
const list = pendingDialogs.get(sessionId);
|
|
11445
|
+
if (!list || list.length === 0) {
|
|
11446
|
+
return { handled: false };
|
|
11447
|
+
}
|
|
11448
|
+
const pending = list.shift();
|
|
11449
|
+
clearTimeout(pending.autoTimer);
|
|
11450
|
+
if (list.length === 0) {
|
|
11451
|
+
pendingDialogs.delete(sessionId);
|
|
11452
|
+
}
|
|
11453
|
+
try {
|
|
11454
|
+
if (action === "accept") {
|
|
11455
|
+
await pending.dialog.accept(promptText);
|
|
11456
|
+
} else {
|
|
11457
|
+
await pending.dialog.dismiss();
|
|
11458
|
+
}
|
|
11459
|
+
} catch {}
|
|
11460
|
+
return { handled: true, dialog: pending.info };
|
|
11461
|
+
}
|
|
11462
|
+
|
|
11463
|
+
// src/lib/session.ts
|
|
11464
|
+
var handles = new Map;
|
|
11465
|
+
async function createSession2(opts = {}) {
|
|
11466
|
+
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
11467
|
+
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
11468
|
+
let browser;
|
|
11469
|
+
let page;
|
|
11470
|
+
if (resolvedEngine === "lightpanda") {
|
|
11471
|
+
browser = await connectLightpanda();
|
|
11472
|
+
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
11473
|
+
page = await context.newPage();
|
|
11474
|
+
} else {
|
|
11475
|
+
browser = await launchPlaywright({
|
|
11476
|
+
headless: opts.headless ?? true,
|
|
11477
|
+
viewport: opts.viewport,
|
|
11478
|
+
userAgent: opts.userAgent
|
|
11479
|
+
});
|
|
11480
|
+
page = await getPage(browser, {
|
|
11481
|
+
viewport: opts.viewport,
|
|
11482
|
+
userAgent: opts.userAgent
|
|
11483
|
+
});
|
|
11484
|
+
}
|
|
11485
|
+
let sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
11486
|
+
try {
|
|
11487
|
+
return new URL(opts.startUrl).hostname;
|
|
11488
|
+
} catch {
|
|
11489
|
+
return;
|
|
11490
|
+
}
|
|
11491
|
+
})() : undefined);
|
|
11492
|
+
const session = createSession({
|
|
11493
|
+
engine: resolvedEngine,
|
|
11494
|
+
projectId: opts.projectId,
|
|
11495
|
+
agentId: opts.agentId,
|
|
11496
|
+
startUrl: opts.startUrl,
|
|
11497
|
+
name: sessionName
|
|
11498
|
+
});
|
|
11499
|
+
if (opts.stealth) {
|
|
11500
|
+
try {
|
|
11501
|
+
await applyStealthPatches(page);
|
|
11502
|
+
} catch {}
|
|
11503
|
+
}
|
|
11504
|
+
const cleanups = [];
|
|
11505
|
+
if (opts.captureNetwork !== false) {
|
|
11506
|
+
try {
|
|
11507
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
11508
|
+
} catch {}
|
|
11509
|
+
}
|
|
11510
|
+
if (opts.captureConsole !== false) {
|
|
11511
|
+
try {
|
|
11512
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
11513
|
+
} catch {}
|
|
11514
|
+
}
|
|
11515
|
+
try {
|
|
11516
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
11517
|
+
} catch {}
|
|
11518
|
+
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
11519
|
+
if (opts.startUrl) {
|
|
11520
|
+
try {
|
|
11521
|
+
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
11522
|
+
} catch {}
|
|
11523
|
+
}
|
|
11524
|
+
return { session, page };
|
|
11525
|
+
}
|
|
11526
|
+
function getSessionPage(sessionId) {
|
|
11527
|
+
const handle = handles.get(sessionId);
|
|
11528
|
+
if (!handle)
|
|
11529
|
+
throw new SessionNotFoundError(sessionId);
|
|
11530
|
+
try {
|
|
11531
|
+
handle.page.url();
|
|
11532
|
+
} catch {
|
|
11533
|
+
handles.delete(sessionId);
|
|
11534
|
+
throw new SessionNotFoundError(sessionId);
|
|
11535
|
+
}
|
|
11536
|
+
return handle.page;
|
|
11537
|
+
}
|
|
11538
|
+
function setSessionPage(sessionId, page) {
|
|
11539
|
+
const handle = handles.get(sessionId);
|
|
11540
|
+
if (!handle)
|
|
11541
|
+
throw new SessionNotFoundError(sessionId);
|
|
11542
|
+
handle.page = page;
|
|
11543
|
+
}
|
|
11544
|
+
async function closeSession2(sessionId) {
|
|
11545
|
+
const handle = handles.get(sessionId);
|
|
11546
|
+
if (handle) {
|
|
11547
|
+
for (const cleanup of handle.cleanups) {
|
|
11548
|
+
try {
|
|
11549
|
+
cleanup();
|
|
11550
|
+
} catch {}
|
|
11551
|
+
}
|
|
11552
|
+
try {
|
|
11553
|
+
await handle.page.context().close();
|
|
11554
|
+
} catch {}
|
|
11555
|
+
try {
|
|
11556
|
+
await closeBrowser(handle.browser);
|
|
11557
|
+
} catch {}
|
|
11558
|
+
handles.delete(sessionId);
|
|
11559
|
+
}
|
|
11560
|
+
return closeSession(sessionId);
|
|
11561
|
+
}
|
|
11562
|
+
function getSession2(sessionId) {
|
|
11563
|
+
return getSession(sessionId);
|
|
11564
|
+
}
|
|
11565
|
+
function listSessions2(filter) {
|
|
11566
|
+
return listSessions(filter);
|
|
11567
|
+
}
|
|
11568
|
+
function getSessionByName2(name) {
|
|
11569
|
+
return getSessionByName(name);
|
|
11570
|
+
}
|
|
11571
|
+
function renameSession2(id, name) {
|
|
11572
|
+
return renameSession(id, name);
|
|
11573
|
+
}
|
|
11574
|
+
function getTokenBudget(sessionId) {
|
|
11575
|
+
const handle = handles.get(sessionId);
|
|
11576
|
+
return handle ? handle.tokenBudget : null;
|
|
11577
|
+
}
|
|
11578
|
+
|
|
11579
|
+
// src/lib/actions.ts
|
|
11580
|
+
init_snapshot();
|
|
11581
|
+
async function click(page, selector, opts) {
|
|
11582
|
+
try {
|
|
11583
|
+
await page.click(selector, {
|
|
11584
|
+
button: opts?.button ?? "left",
|
|
11585
|
+
clickCount: opts?.clickCount ?? 1,
|
|
11586
|
+
delay: opts?.delay,
|
|
11587
|
+
timeout: opts?.timeout ?? 1e4
|
|
11588
|
+
});
|
|
11589
|
+
} catch (err) {
|
|
11590
|
+
if (err instanceof Error && err.message.includes("not found")) {
|
|
11591
|
+
throw new ElementNotFoundError(selector);
|
|
10967
11592
|
}
|
|
10968
11593
|
throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
|
|
10969
11594
|
}
|
|
@@ -11173,6 +11798,63 @@ function stopWatch(watchId) {
|
|
|
11173
11798
|
activeWatches.delete(watchId);
|
|
11174
11799
|
}
|
|
11175
11800
|
}
|
|
11801
|
+
async function clickRef(page, sessionId, ref, opts) {
|
|
11802
|
+
try {
|
|
11803
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
11804
|
+
await locator.click({ timeout: opts?.timeout ?? 1e4 });
|
|
11805
|
+
} catch (err) {
|
|
11806
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
11807
|
+
throw new ElementNotFoundError(ref);
|
|
11808
|
+
if (err instanceof Error && err.message.includes("No snapshot"))
|
|
11809
|
+
throw new BrowserError(err.message, "NO_SNAPSHOT");
|
|
11810
|
+
throw new BrowserError(`clickRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CLICK_REF_FAILED");
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
async function typeRef(page, sessionId, ref, text, opts) {
|
|
11814
|
+
try {
|
|
11815
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
11816
|
+
if (opts?.clear)
|
|
11817
|
+
await locator.fill("", { timeout: opts.timeout ?? 1e4 });
|
|
11818
|
+
await locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
11819
|
+
} catch (err) {
|
|
11820
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
11821
|
+
throw new ElementNotFoundError(ref);
|
|
11822
|
+
throw new BrowserError(`typeRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "TYPE_REF_FAILED");
|
|
11823
|
+
}
|
|
11824
|
+
}
|
|
11825
|
+
async function selectRef(page, sessionId, ref, value, timeout = 1e4) {
|
|
11826
|
+
try {
|
|
11827
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
11828
|
+
return await locator.selectOption(value, { timeout });
|
|
11829
|
+
} catch (err) {
|
|
11830
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
11831
|
+
throw new ElementNotFoundError(ref);
|
|
11832
|
+
throw new BrowserError(`selectRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "SELECT_REF_FAILED");
|
|
11833
|
+
}
|
|
11834
|
+
}
|
|
11835
|
+
async function checkRef(page, sessionId, ref, checked, timeout = 1e4) {
|
|
11836
|
+
try {
|
|
11837
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
11838
|
+
if (checked)
|
|
11839
|
+
await locator.check({ timeout });
|
|
11840
|
+
else
|
|
11841
|
+
await locator.uncheck({ timeout });
|
|
11842
|
+
} catch (err) {
|
|
11843
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
11844
|
+
throw new ElementNotFoundError(ref);
|
|
11845
|
+
throw new BrowserError(`checkRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CHECK_REF_FAILED");
|
|
11846
|
+
}
|
|
11847
|
+
}
|
|
11848
|
+
async function hoverRef(page, sessionId, ref, timeout = 1e4) {
|
|
11849
|
+
try {
|
|
11850
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
11851
|
+
await locator.hover({ timeout });
|
|
11852
|
+
} catch (err) {
|
|
11853
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
11854
|
+
throw new ElementNotFoundError(ref);
|
|
11855
|
+
throw new BrowserError(`hoverRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "HOVER_REF_FAILED");
|
|
11856
|
+
}
|
|
11857
|
+
}
|
|
11176
11858
|
|
|
11177
11859
|
// src/lib/extractor.ts
|
|
11178
11860
|
async function getText(page, selector) {
|
|
@@ -11239,24 +11921,6 @@ async function extractTable(page, selector) {
|
|
|
11239
11921
|
return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
|
|
11240
11922
|
}, selector);
|
|
11241
11923
|
}
|
|
11242
|
-
async function getAriaSnapshot(page) {
|
|
11243
|
-
try {
|
|
11244
|
-
return await page.ariaSnapshot?.() ?? page.evaluate(() => {
|
|
11245
|
-
function walk(el, indent = 0) {
|
|
11246
|
-
const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
11247
|
-
const label = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 50) ?? "";
|
|
11248
|
-
const line = " ".repeat(indent) + `[${role}] ${label}`;
|
|
11249
|
-
const children = Array.from(el.children).map((c) => walk(c, indent + 1)).join(`
|
|
11250
|
-
`);
|
|
11251
|
-
return children ? `${line}
|
|
11252
|
-
${children}` : line;
|
|
11253
|
-
}
|
|
11254
|
-
return walk(document.body);
|
|
11255
|
-
});
|
|
11256
|
-
} catch {
|
|
11257
|
-
return page.evaluate(() => document.body.innerText?.slice(0, 2000) ?? "");
|
|
11258
|
-
}
|
|
11259
|
-
}
|
|
11260
11924
|
async function extract(page, opts = {}) {
|
|
11261
11925
|
const result = {};
|
|
11262
11926
|
const format = opts.format ?? "text";
|
|
@@ -11325,7 +11989,7 @@ import { homedir as homedir2 } from "os";
|
|
|
11325
11989
|
|
|
11326
11990
|
// src/db/gallery.ts
|
|
11327
11991
|
init_schema();
|
|
11328
|
-
import { randomUUID as
|
|
11992
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
11329
11993
|
function deserialize(row) {
|
|
11330
11994
|
return {
|
|
11331
11995
|
id: row.id,
|
|
@@ -11349,7 +12013,7 @@ function deserialize(row) {
|
|
|
11349
12013
|
}
|
|
11350
12014
|
function createEntry(data) {
|
|
11351
12015
|
const db = getDatabase();
|
|
11352
|
-
const id =
|
|
12016
|
+
const id = randomUUID4();
|
|
11353
12017
|
db.prepare(`
|
|
11354
12018
|
INSERT INTO gallery_entries
|
|
11355
12019
|
(id, session_id, project_id, url, title, path, thumbnail_path, format,
|
|
@@ -11517,11 +12181,20 @@ async function takeScreenshot(page, opts) {
|
|
|
11517
12181
|
}
|
|
11518
12182
|
const originalSizeBytes = rawBuffer.length;
|
|
11519
12183
|
let finalBuffer;
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
|
|
11523
|
-
|
|
11524
|
-
|
|
12184
|
+
let compressed = true;
|
|
12185
|
+
let fallback = false;
|
|
12186
|
+
try {
|
|
12187
|
+
if (compress && format !== "png") {
|
|
12188
|
+
finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
|
|
12189
|
+
} else if (compress && format === "png") {
|
|
12190
|
+
finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
|
|
12191
|
+
} else {
|
|
12192
|
+
finalBuffer = rawBuffer;
|
|
12193
|
+
compressed = false;
|
|
12194
|
+
}
|
|
12195
|
+
} catch (sharpErr) {
|
|
12196
|
+
fallback = true;
|
|
12197
|
+
compressed = false;
|
|
11525
12198
|
finalBuffer = rawBuffer;
|
|
11526
12199
|
}
|
|
11527
12200
|
const compressedSizeBytes = finalBuffer.length;
|
|
@@ -11549,7 +12222,8 @@ async function takeScreenshot(page, opts) {
|
|
|
11549
12222
|
compressed_size_bytes: compressedSizeBytes,
|
|
11550
12223
|
compression_ratio: compressionRatio,
|
|
11551
12224
|
thumbnail_path: thumbnailPath,
|
|
11552
|
-
thumbnail_base64: thumbnailBase64
|
|
12225
|
+
thumbnail_base64: thumbnailBase64,
|
|
12226
|
+
...fallback ? { fallback: true, compressed: false } : {}
|
|
11553
12227
|
};
|
|
11554
12228
|
if (opts?.track !== false) {
|
|
11555
12229
|
try {
|
|
@@ -11596,141 +12270,17 @@ async function generatePDF(page, opts) {
|
|
|
11596
12270
|
path: pdfPath,
|
|
11597
12271
|
format: opts?.format ?? "A4",
|
|
11598
12272
|
landscape: opts?.landscape ?? false,
|
|
11599
|
-
margin: opts?.margin,
|
|
11600
|
-
printBackground: opts?.printBackground ?? true
|
|
11601
|
-
});
|
|
11602
|
-
return {
|
|
11603
|
-
path: pdfPath,
|
|
11604
|
-
base64: Buffer.from(buffer).toString("base64"),
|
|
11605
|
-
size_bytes: buffer.length
|
|
11606
|
-
};
|
|
11607
|
-
} catch (err) {
|
|
11608
|
-
throw new BrowserError(`PDF generation failed: ${err instanceof Error ? err.message : String(err)}`, "PDF_FAILED");
|
|
11609
|
-
}
|
|
11610
|
-
}
|
|
11611
|
-
|
|
11612
|
-
// src/db/network-log.ts
|
|
11613
|
-
init_schema();
|
|
11614
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
11615
|
-
function logRequest(data) {
|
|
11616
|
-
const db = getDatabase();
|
|
11617
|
-
const id = randomUUID3();
|
|
11618
|
-
db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
|
|
11619
|
-
response_headers, request_body, body_size, duration_ms, resource_type)
|
|
11620
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
|
|
11621
|
-
return getNetworkRequest(id);
|
|
11622
|
-
}
|
|
11623
|
-
function getNetworkRequest(id) {
|
|
11624
|
-
const db = getDatabase();
|
|
11625
|
-
return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
|
|
11626
|
-
}
|
|
11627
|
-
function getNetworkLog(sessionId) {
|
|
11628
|
-
const db = getDatabase();
|
|
11629
|
-
return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
11630
|
-
}
|
|
11631
|
-
|
|
11632
|
-
// src/lib/network.ts
|
|
11633
|
-
function enableNetworkLogging(page, sessionId) {
|
|
11634
|
-
const requestStart = new Map;
|
|
11635
|
-
const onRequest = (req) => {
|
|
11636
|
-
requestStart.set(req.url(), Date.now());
|
|
11637
|
-
};
|
|
11638
|
-
const onResponse = (res) => {
|
|
11639
|
-
const start = requestStart.get(res.url()) ?? Date.now();
|
|
11640
|
-
const duration = Date.now() - start;
|
|
11641
|
-
const req = res.request();
|
|
11642
|
-
try {
|
|
11643
|
-
logRequest({
|
|
11644
|
-
session_id: sessionId,
|
|
11645
|
-
method: req.method(),
|
|
11646
|
-
url: res.url(),
|
|
11647
|
-
status_code: res.status(),
|
|
11648
|
-
request_headers: JSON.stringify(req.headers()),
|
|
11649
|
-
response_headers: JSON.stringify(res.headers()),
|
|
11650
|
-
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
11651
|
-
duration_ms: duration,
|
|
11652
|
-
resource_type: req.resourceType()
|
|
11653
|
-
});
|
|
11654
|
-
} catch {}
|
|
11655
|
-
};
|
|
11656
|
-
page.on("request", onRequest);
|
|
11657
|
-
page.on("response", onResponse);
|
|
11658
|
-
return () => {
|
|
11659
|
-
page.off("request", onRequest);
|
|
11660
|
-
page.off("response", onResponse);
|
|
11661
|
-
};
|
|
11662
|
-
}
|
|
11663
|
-
async function addInterceptRule(page, rule) {
|
|
11664
|
-
await page.route(rule.pattern, async (route) => {
|
|
11665
|
-
if (rule.action === "block") {
|
|
11666
|
-
await route.abort();
|
|
11667
|
-
} else if (rule.action === "modify" && rule.response) {
|
|
11668
|
-
await route.fulfill({
|
|
11669
|
-
status: rule.response.status,
|
|
11670
|
-
body: rule.response.body,
|
|
11671
|
-
headers: rule.response.headers
|
|
11672
|
-
});
|
|
11673
|
-
} else {
|
|
11674
|
-
await route.continue();
|
|
11675
|
-
}
|
|
11676
|
-
});
|
|
11677
|
-
}
|
|
11678
|
-
function startHAR(page) {
|
|
11679
|
-
const entries = [];
|
|
11680
|
-
const requestStart = new Map;
|
|
11681
|
-
const onRequest = (req) => {
|
|
11682
|
-
requestStart.set(req.url() + req.method(), {
|
|
11683
|
-
time: Date.now(),
|
|
11684
|
-
method: req.method(),
|
|
11685
|
-
headers: req.headers(),
|
|
11686
|
-
postData: req.postData() ?? undefined
|
|
11687
|
-
});
|
|
11688
|
-
};
|
|
11689
|
-
const onResponse = async (res) => {
|
|
11690
|
-
const key = res.url() + res.request().method();
|
|
11691
|
-
const start = requestStart.get(key);
|
|
11692
|
-
if (!start)
|
|
11693
|
-
return;
|
|
11694
|
-
const duration = Date.now() - start.time;
|
|
11695
|
-
const entry = {
|
|
11696
|
-
startedDateTime: new Date(start.time).toISOString(),
|
|
11697
|
-
time: duration,
|
|
11698
|
-
request: {
|
|
11699
|
-
method: start.method,
|
|
11700
|
-
url: res.url(),
|
|
11701
|
-
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
11702
|
-
postData: start.postData ? { text: start.postData } : undefined
|
|
11703
|
-
},
|
|
11704
|
-
response: {
|
|
11705
|
-
status: res.status(),
|
|
11706
|
-
statusText: res.statusText(),
|
|
11707
|
-
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
11708
|
-
content: {
|
|
11709
|
-
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
11710
|
-
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
11711
|
-
}
|
|
11712
|
-
},
|
|
11713
|
-
timings: { send: 0, wait: duration, receive: 0 }
|
|
11714
|
-
};
|
|
11715
|
-
entries.push(entry);
|
|
11716
|
-
requestStart.delete(key);
|
|
11717
|
-
};
|
|
11718
|
-
page.on("request", onRequest);
|
|
11719
|
-
page.on("response", onResponse);
|
|
11720
|
-
return {
|
|
11721
|
-
entries,
|
|
11722
|
-
stop: () => {
|
|
11723
|
-
page.off("request", onRequest);
|
|
11724
|
-
page.off("response", onResponse);
|
|
11725
|
-
return {
|
|
11726
|
-
log: {
|
|
11727
|
-
version: "1.2",
|
|
11728
|
-
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
11729
|
-
entries
|
|
11730
|
-
}
|
|
11731
|
-
};
|
|
11732
|
-
}
|
|
11733
|
-
};
|
|
12273
|
+
margin: opts?.margin,
|
|
12274
|
+
printBackground: opts?.printBackground ?? true
|
|
12275
|
+
});
|
|
12276
|
+
return {
|
|
12277
|
+
path: pdfPath,
|
|
12278
|
+
base64: Buffer.from(buffer).toString("base64"),
|
|
12279
|
+
size_bytes: buffer.length
|
|
12280
|
+
};
|
|
12281
|
+
} catch (err) {
|
|
12282
|
+
throw new BrowserError(`PDF generation failed: ${err instanceof Error ? err.message : String(err)}`, "PDF_FAILED");
|
|
12283
|
+
}
|
|
11734
12284
|
}
|
|
11735
12285
|
|
|
11736
12286
|
// src/engines/cdp.ts
|
|
@@ -11878,34 +12428,6 @@ async function getPerformanceMetrics(page) {
|
|
|
11878
12428
|
};
|
|
11879
12429
|
}
|
|
11880
12430
|
|
|
11881
|
-
// src/lib/console.ts
|
|
11882
|
-
init_console_log();
|
|
11883
|
-
function enableConsoleCapture(page, sessionId) {
|
|
11884
|
-
const onConsole = (msg) => {
|
|
11885
|
-
const levelMap = {
|
|
11886
|
-
log: "log",
|
|
11887
|
-
warn: "warn",
|
|
11888
|
-
error: "error",
|
|
11889
|
-
debug: "debug",
|
|
11890
|
-
info: "info",
|
|
11891
|
-
warning: "warn"
|
|
11892
|
-
};
|
|
11893
|
-
const level = levelMap[msg.type()] ?? "log";
|
|
11894
|
-
const location = msg.location();
|
|
11895
|
-
try {
|
|
11896
|
-
logConsoleMessage({
|
|
11897
|
-
session_id: sessionId,
|
|
11898
|
-
level,
|
|
11899
|
-
message: msg.text(),
|
|
11900
|
-
source: location.url || undefined,
|
|
11901
|
-
line_number: location.lineNumber || undefined
|
|
11902
|
-
});
|
|
11903
|
-
} catch {}
|
|
11904
|
-
};
|
|
11905
|
-
page.on("console", onConsole);
|
|
11906
|
-
return () => page.off("console", onConsole);
|
|
11907
|
-
}
|
|
11908
|
-
|
|
11909
12431
|
// src/lib/storage.ts
|
|
11910
12432
|
async function getCookies(page, filter) {
|
|
11911
12433
|
const cookies = await page.context().cookies();
|
|
@@ -12455,6 +12977,9 @@ async function diffImages(path1, path2) {
|
|
|
12455
12977
|
};
|
|
12456
12978
|
}
|
|
12457
12979
|
|
|
12980
|
+
// src/mcp/index.ts
|
|
12981
|
+
init_snapshot();
|
|
12982
|
+
|
|
12458
12983
|
// src/lib/files-integration.ts
|
|
12459
12984
|
import { join as join5 } from "path";
|
|
12460
12985
|
import { mkdirSync as mkdirSync5, copyFileSync as copyFileSync2 } from "fs";
|
|
@@ -12482,7 +13007,224 @@ async function persistFile(localPath, opts) {
|
|
|
12482
13007
|
};
|
|
12483
13008
|
}
|
|
12484
13009
|
|
|
13010
|
+
// src/lib/tabs.ts
|
|
13011
|
+
async function newTab(page, url) {
|
|
13012
|
+
const context = page.context();
|
|
13013
|
+
const newPage = await context.newPage();
|
|
13014
|
+
if (url) {
|
|
13015
|
+
await newPage.goto(url, { waitUntil: "domcontentloaded" });
|
|
13016
|
+
}
|
|
13017
|
+
const pages = context.pages();
|
|
13018
|
+
const index = pages.indexOf(newPage);
|
|
13019
|
+
return {
|
|
13020
|
+
index,
|
|
13021
|
+
url: newPage.url(),
|
|
13022
|
+
title: await newPage.title(),
|
|
13023
|
+
is_active: true
|
|
13024
|
+
};
|
|
13025
|
+
}
|
|
13026
|
+
async function listTabs(page) {
|
|
13027
|
+
const context = page.context();
|
|
13028
|
+
const pages = context.pages();
|
|
13029
|
+
const activePage = page;
|
|
13030
|
+
const tabs = [];
|
|
13031
|
+
for (let i = 0;i < pages.length; i++) {
|
|
13032
|
+
let url = "";
|
|
13033
|
+
let title = "";
|
|
13034
|
+
try {
|
|
13035
|
+
url = pages[i].url();
|
|
13036
|
+
title = await pages[i].title();
|
|
13037
|
+
} catch {}
|
|
13038
|
+
tabs.push({
|
|
13039
|
+
index: i,
|
|
13040
|
+
url,
|
|
13041
|
+
title,
|
|
13042
|
+
is_active: pages[i] === activePage
|
|
13043
|
+
});
|
|
13044
|
+
}
|
|
13045
|
+
return tabs;
|
|
13046
|
+
}
|
|
13047
|
+
async function switchTab(page, index) {
|
|
13048
|
+
const context = page.context();
|
|
13049
|
+
const pages = context.pages();
|
|
13050
|
+
if (index < 0 || index >= pages.length) {
|
|
13051
|
+
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
13052
|
+
}
|
|
13053
|
+
const targetPage = pages[index];
|
|
13054
|
+
await targetPage.bringToFront();
|
|
13055
|
+
return {
|
|
13056
|
+
page: targetPage,
|
|
13057
|
+
tab: {
|
|
13058
|
+
index,
|
|
13059
|
+
url: targetPage.url(),
|
|
13060
|
+
title: await targetPage.title(),
|
|
13061
|
+
is_active: true
|
|
13062
|
+
}
|
|
13063
|
+
};
|
|
13064
|
+
}
|
|
13065
|
+
async function closeTab(page, index) {
|
|
13066
|
+
const context = page.context();
|
|
13067
|
+
const pages = context.pages();
|
|
13068
|
+
if (index < 0 || index >= pages.length) {
|
|
13069
|
+
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
13070
|
+
}
|
|
13071
|
+
if (pages.length <= 1) {
|
|
13072
|
+
throw new Error("Cannot close the last tab");
|
|
13073
|
+
}
|
|
13074
|
+
const targetPage = pages[index];
|
|
13075
|
+
const isActivePage = targetPage === page;
|
|
13076
|
+
await targetPage.close();
|
|
13077
|
+
const remainingPages = context.pages();
|
|
13078
|
+
const activeIndex = isActivePage ? Math.min(index, remainingPages.length - 1) : remainingPages.indexOf(page);
|
|
13079
|
+
const activePage = remainingPages[activeIndex >= 0 ? activeIndex : 0];
|
|
13080
|
+
return {
|
|
13081
|
+
closed_index: index,
|
|
13082
|
+
active_tab: {
|
|
13083
|
+
index: activeIndex >= 0 ? activeIndex : 0,
|
|
13084
|
+
url: activePage.url(),
|
|
13085
|
+
title: await activePage.title(),
|
|
13086
|
+
is_active: true
|
|
13087
|
+
}
|
|
13088
|
+
};
|
|
13089
|
+
}
|
|
13090
|
+
|
|
13091
|
+
// src/lib/profiles.ts
|
|
13092
|
+
import { mkdirSync as mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
13093
|
+
import { join as join6 } from "path";
|
|
13094
|
+
import { homedir as homedir6 } from "os";
|
|
13095
|
+
function getProfilesDir() {
|
|
13096
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
13097
|
+
const dir = join6(dataDir, "profiles");
|
|
13098
|
+
mkdirSync6(dir, { recursive: true });
|
|
13099
|
+
return dir;
|
|
13100
|
+
}
|
|
13101
|
+
function getProfileDir(name) {
|
|
13102
|
+
return join6(getProfilesDir(), name);
|
|
13103
|
+
}
|
|
13104
|
+
async function saveProfile(page, name) {
|
|
13105
|
+
const dir = getProfileDir(name);
|
|
13106
|
+
mkdirSync6(dir, { recursive: true });
|
|
13107
|
+
const cookies = await page.context().cookies();
|
|
13108
|
+
writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
13109
|
+
let localStorage2 = {};
|
|
13110
|
+
try {
|
|
13111
|
+
localStorage2 = await page.evaluate(() => {
|
|
13112
|
+
const result = {};
|
|
13113
|
+
for (let i = 0;i < window.localStorage.length; i++) {
|
|
13114
|
+
const key = window.localStorage.key(i);
|
|
13115
|
+
result[key] = window.localStorage.getItem(key);
|
|
13116
|
+
}
|
|
13117
|
+
return result;
|
|
13118
|
+
});
|
|
13119
|
+
} catch {}
|
|
13120
|
+
writeFileSync2(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
13121
|
+
const savedAt = new Date().toISOString();
|
|
13122
|
+
const url = page.url();
|
|
13123
|
+
const meta = { saved_at: savedAt, url };
|
|
13124
|
+
writeFileSync2(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
13125
|
+
return {
|
|
13126
|
+
name,
|
|
13127
|
+
saved_at: savedAt,
|
|
13128
|
+
url,
|
|
13129
|
+
cookie_count: cookies.length,
|
|
13130
|
+
storage_key_count: Object.keys(localStorage2).length
|
|
13131
|
+
};
|
|
13132
|
+
}
|
|
13133
|
+
function loadProfile(name) {
|
|
13134
|
+
const dir = getProfileDir(name);
|
|
13135
|
+
if (!existsSync3(dir)) {
|
|
13136
|
+
throw new Error(`Profile not found: ${name}`);
|
|
13137
|
+
}
|
|
13138
|
+
const cookiesPath = join6(dir, "cookies.json");
|
|
13139
|
+
const storagePath = join6(dir, "storage.json");
|
|
13140
|
+
const metaPath2 = join6(dir, "meta.json");
|
|
13141
|
+
const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
13142
|
+
const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
13143
|
+
let savedAt = new Date().toISOString();
|
|
13144
|
+
let url;
|
|
13145
|
+
if (existsSync3(metaPath2)) {
|
|
13146
|
+
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
13147
|
+
savedAt = meta.saved_at ?? savedAt;
|
|
13148
|
+
url = meta.url;
|
|
13149
|
+
}
|
|
13150
|
+
return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
|
|
13151
|
+
}
|
|
13152
|
+
async function applyProfile(page, profileData) {
|
|
13153
|
+
if (profileData.cookies.length > 0) {
|
|
13154
|
+
await page.context().addCookies(profileData.cookies);
|
|
13155
|
+
}
|
|
13156
|
+
const storageKeys = Object.keys(profileData.localStorage);
|
|
13157
|
+
if (storageKeys.length > 0) {
|
|
13158
|
+
try {
|
|
13159
|
+
await page.evaluate((storage) => {
|
|
13160
|
+
for (const [key, value] of Object.entries(storage)) {
|
|
13161
|
+
window.localStorage.setItem(key, value);
|
|
13162
|
+
}
|
|
13163
|
+
}, profileData.localStorage);
|
|
13164
|
+
} catch {}
|
|
13165
|
+
}
|
|
13166
|
+
return {
|
|
13167
|
+
cookies_applied: profileData.cookies.length,
|
|
13168
|
+
storage_keys_applied: storageKeys.length
|
|
13169
|
+
};
|
|
13170
|
+
}
|
|
13171
|
+
function listProfiles() {
|
|
13172
|
+
const dir = getProfilesDir();
|
|
13173
|
+
if (!existsSync3(dir))
|
|
13174
|
+
return [];
|
|
13175
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
13176
|
+
const profiles = [];
|
|
13177
|
+
for (const entry of entries) {
|
|
13178
|
+
if (!entry.isDirectory())
|
|
13179
|
+
continue;
|
|
13180
|
+
const name = entry.name;
|
|
13181
|
+
const profileDir = join6(dir, name);
|
|
13182
|
+
let savedAt = "";
|
|
13183
|
+
let url;
|
|
13184
|
+
let cookieCount = 0;
|
|
13185
|
+
let storageKeyCount = 0;
|
|
13186
|
+
try {
|
|
13187
|
+
const metaPath2 = join6(profileDir, "meta.json");
|
|
13188
|
+
if (existsSync3(metaPath2)) {
|
|
13189
|
+
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
13190
|
+
savedAt = meta.saved_at ?? "";
|
|
13191
|
+
url = meta.url;
|
|
13192
|
+
}
|
|
13193
|
+
const cookiesPath = join6(profileDir, "cookies.json");
|
|
13194
|
+
if (existsSync3(cookiesPath)) {
|
|
13195
|
+
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
13196
|
+
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
13197
|
+
}
|
|
13198
|
+
const storagePath = join6(profileDir, "storage.json");
|
|
13199
|
+
if (existsSync3(storagePath)) {
|
|
13200
|
+
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
13201
|
+
storageKeyCount = Object.keys(storage).length;
|
|
13202
|
+
}
|
|
13203
|
+
} catch {}
|
|
13204
|
+
profiles.push({
|
|
13205
|
+
name,
|
|
13206
|
+
saved_at: savedAt,
|
|
13207
|
+
url,
|
|
13208
|
+
cookie_count: cookieCount,
|
|
13209
|
+
storage_key_count: storageKeyCount
|
|
13210
|
+
});
|
|
13211
|
+
}
|
|
13212
|
+
return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
|
|
13213
|
+
}
|
|
13214
|
+
function deleteProfile(name) {
|
|
13215
|
+
const dir = getProfileDir(name);
|
|
13216
|
+
if (!existsSync3(dir))
|
|
13217
|
+
return false;
|
|
13218
|
+
try {
|
|
13219
|
+
rmSync(dir, { recursive: true, force: true });
|
|
13220
|
+
return true;
|
|
13221
|
+
} catch {
|
|
13222
|
+
return false;
|
|
13223
|
+
}
|
|
13224
|
+
}
|
|
13225
|
+
|
|
12485
13226
|
// src/mcp/index.ts
|
|
13227
|
+
var _pkg = JSON.parse(readFileSync3(join7(import.meta.dir, "../../package.json"), "utf8"));
|
|
12486
13228
|
var networkLogCleanup = new Map;
|
|
12487
13229
|
var consoleCaptureCleanup = new Map;
|
|
12488
13230
|
var harCaptures = new Map;
|
|
@@ -12509,8 +13251,9 @@ server.tool("browser_session_create", "Create a new browser session with the spe
|
|
|
12509
13251
|
start_url: exports_external.string().optional(),
|
|
12510
13252
|
headless: exports_external.boolean().optional().default(true),
|
|
12511
13253
|
viewport_width: exports_external.number().optional().default(1280),
|
|
12512
|
-
viewport_height: exports_external.number().optional().default(720)
|
|
12513
|
-
|
|
13254
|
+
viewport_height: exports_external.number().optional().default(720),
|
|
13255
|
+
stealth: exports_external.boolean().optional().default(false)
|
|
13256
|
+
}, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
|
|
12514
13257
|
try {
|
|
12515
13258
|
const { session } = await createSession2({
|
|
12516
13259
|
engine,
|
|
@@ -12519,7 +13262,8 @@ server.tool("browser_session_create", "Create a new browser session with the spe
|
|
|
12519
13262
|
agentId: agent_id,
|
|
12520
13263
|
startUrl: start_url,
|
|
12521
13264
|
headless,
|
|
12522
|
-
viewport: { width: viewport_width, height: viewport_height }
|
|
13265
|
+
viewport: { width: viewport_width, height: viewport_height },
|
|
13266
|
+
stealth
|
|
12523
13267
|
});
|
|
12524
13268
|
return json({ session });
|
|
12525
13269
|
} catch (e) {
|
|
@@ -12546,11 +13290,67 @@ server.tool("browser_session_close", "Close a browser session", { session_id: ex
|
|
|
12546
13290
|
return err(e);
|
|
12547
13291
|
}
|
|
12548
13292
|
});
|
|
12549
|
-
server.tool("browser_navigate", "Navigate to a URL
|
|
13293
|
+
server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
|
|
13294
|
+
session_id: exports_external.string(),
|
|
13295
|
+
url: exports_external.string(),
|
|
13296
|
+
timeout: exports_external.number().optional().default(30000),
|
|
13297
|
+
auto_snapshot: exports_external.boolean().optional().default(true),
|
|
13298
|
+
auto_thumbnail: exports_external.boolean().optional().default(true)
|
|
13299
|
+
}, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
|
|
12550
13300
|
try {
|
|
12551
13301
|
const page = getSessionPage(session_id);
|
|
12552
13302
|
await navigate(page, url, timeout);
|
|
12553
|
-
|
|
13303
|
+
const title = await getTitle(page);
|
|
13304
|
+
const current_url = await getUrl(page);
|
|
13305
|
+
const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
|
|
13306
|
+
let redirect_type;
|
|
13307
|
+
if (redirected) {
|
|
13308
|
+
try {
|
|
13309
|
+
const reqHost = new URL(url).hostname;
|
|
13310
|
+
const resHost = new URL(current_url).hostname;
|
|
13311
|
+
const reqPath = new URL(url).pathname;
|
|
13312
|
+
const resPath = new URL(current_url).pathname;
|
|
13313
|
+
if (reqHost !== resHost)
|
|
13314
|
+
redirect_type = "canonical";
|
|
13315
|
+
else if (resPath.match(/\/[a-z]{2}-[a-z]{2}\//))
|
|
13316
|
+
redirect_type = "geo";
|
|
13317
|
+
else if (current_url.includes("login") || current_url.includes("signin"))
|
|
13318
|
+
redirect_type = "auth";
|
|
13319
|
+
else
|
|
13320
|
+
redirect_type = "unknown";
|
|
13321
|
+
} catch {}
|
|
13322
|
+
}
|
|
13323
|
+
try {
|
|
13324
|
+
const session = getSession2(session_id);
|
|
13325
|
+
if (!session.name) {
|
|
13326
|
+
const hostname = new URL(current_url).hostname;
|
|
13327
|
+
renameSession2(session_id, hostname);
|
|
13328
|
+
}
|
|
13329
|
+
} catch {}
|
|
13330
|
+
const result = {
|
|
13331
|
+
url,
|
|
13332
|
+
title,
|
|
13333
|
+
current_url,
|
|
13334
|
+
redirected,
|
|
13335
|
+
...redirect_type ? { redirect_type } : {}
|
|
13336
|
+
};
|
|
13337
|
+
if (auto_thumbnail) {
|
|
13338
|
+
try {
|
|
13339
|
+
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
13340
|
+
result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
13341
|
+
} catch {}
|
|
13342
|
+
}
|
|
13343
|
+
if (auto_snapshot) {
|
|
13344
|
+
try {
|
|
13345
|
+
const snap = await takeSnapshot(page, session_id);
|
|
13346
|
+
setLastSnapshot(session_id, snap);
|
|
13347
|
+
const refEntries = Object.entries(snap.refs).slice(0, 30);
|
|
13348
|
+
result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
13349
|
+
result.interactive_count = snap.interactive_count;
|
|
13350
|
+
result.has_errors = getConsoleLog(session_id, "error").length > 0;
|
|
13351
|
+
} catch {}
|
|
13352
|
+
}
|
|
13353
|
+
return json(result);
|
|
12554
13354
|
} catch (e) {
|
|
12555
13355
|
return err(e);
|
|
12556
13356
|
}
|
|
@@ -12582,29 +13382,47 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
|
|
|
12582
13382
|
return err(e);
|
|
12583
13383
|
}
|
|
12584
13384
|
});
|
|
12585
|
-
server.tool("browser_click", "Click an element
|
|
13385
|
+
server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
|
|
12586
13386
|
try {
|
|
12587
13387
|
const page = getSessionPage(session_id);
|
|
13388
|
+
if (ref) {
|
|
13389
|
+
await clickRef(page, session_id, ref, { timeout });
|
|
13390
|
+
return json({ clicked: ref, method: "ref" });
|
|
13391
|
+
}
|
|
13392
|
+
if (!selector)
|
|
13393
|
+
return err(new Error("Either ref or selector is required"));
|
|
12588
13394
|
await click(page, selector, { button, timeout });
|
|
12589
|
-
return json({ clicked: selector });
|
|
13395
|
+
return json({ clicked: selector, method: "selector" });
|
|
12590
13396
|
} catch (e) {
|
|
12591
13397
|
return err(e);
|
|
12592
13398
|
}
|
|
12593
13399
|
});
|
|
12594
|
-
server.tool("browser_type", "Type text into an element", { session_id: exports_external.string(), selector: exports_external.string(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, text, clear, delay }) => {
|
|
13400
|
+
server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
|
|
12595
13401
|
try {
|
|
12596
13402
|
const page = getSessionPage(session_id);
|
|
13403
|
+
if (ref) {
|
|
13404
|
+
await typeRef(page, session_id, ref, text, { clear, delay });
|
|
13405
|
+
return json({ typed: text, ref, method: "ref" });
|
|
13406
|
+
}
|
|
13407
|
+
if (!selector)
|
|
13408
|
+
return err(new Error("Either ref or selector is required"));
|
|
12597
13409
|
await type(page, selector, text, { clear, delay });
|
|
12598
|
-
return json({ typed: text, selector });
|
|
13410
|
+
return json({ typed: text, selector, method: "selector" });
|
|
12599
13411
|
} catch (e) {
|
|
12600
13412
|
return err(e);
|
|
12601
13413
|
}
|
|
12602
13414
|
});
|
|
12603
|
-
server.tool("browser_hover", "Hover over an element", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
|
|
13415
|
+
server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
|
|
12604
13416
|
try {
|
|
12605
13417
|
const page = getSessionPage(session_id);
|
|
13418
|
+
if (ref) {
|
|
13419
|
+
await hoverRef(page, session_id, ref);
|
|
13420
|
+
return json({ hovered: ref, method: "ref" });
|
|
13421
|
+
}
|
|
13422
|
+
if (!selector)
|
|
13423
|
+
return err(new Error("Either ref or selector is required"));
|
|
12606
13424
|
await hover(page, selector);
|
|
12607
|
-
return json({ hovered: selector });
|
|
13425
|
+
return json({ hovered: selector, method: "selector" });
|
|
12608
13426
|
} catch (e) {
|
|
12609
13427
|
return err(e);
|
|
12610
13428
|
}
|
|
@@ -12618,20 +13436,32 @@ server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.
|
|
|
12618
13436
|
return err(e);
|
|
12619
13437
|
}
|
|
12620
13438
|
});
|
|
12621
|
-
server.tool("browser_select", "Select a dropdown option", { session_id: exports_external.string(), selector: exports_external.string(), value: exports_external.string() }, async ({ session_id, selector, value }) => {
|
|
13439
|
+
server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
|
|
12622
13440
|
try {
|
|
12623
13441
|
const page = getSessionPage(session_id);
|
|
13442
|
+
if (ref) {
|
|
13443
|
+
const selected2 = await selectRef(page, session_id, ref, value);
|
|
13444
|
+
return json({ selected: selected2, method: "ref" });
|
|
13445
|
+
}
|
|
13446
|
+
if (!selector)
|
|
13447
|
+
return err(new Error("Either ref or selector is required"));
|
|
12624
13448
|
const selected = await selectOption(page, selector, value);
|
|
12625
|
-
return json({ selected });
|
|
13449
|
+
return json({ selected, method: "selector" });
|
|
12626
13450
|
} catch (e) {
|
|
12627
13451
|
return err(e);
|
|
12628
13452
|
}
|
|
12629
13453
|
});
|
|
12630
|
-
server.tool("
|
|
13454
|
+
server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
|
|
12631
13455
|
try {
|
|
12632
13456
|
const page = getSessionPage(session_id);
|
|
13457
|
+
if (ref) {
|
|
13458
|
+
await checkRef(page, session_id, ref, checked);
|
|
13459
|
+
return json({ checked, ref, method: "ref" });
|
|
13460
|
+
}
|
|
13461
|
+
if (!selector)
|
|
13462
|
+
return err(new Error("Either ref or selector is required"));
|
|
12633
13463
|
await checkBox(page, selector, checked);
|
|
12634
|
-
return json({ checked, selector });
|
|
13464
|
+
return json({ checked, selector, method: "selector" });
|
|
12635
13465
|
} catch (e) {
|
|
12636
13466
|
return err(e);
|
|
12637
13467
|
}
|
|
@@ -12712,15 +13542,38 @@ server.tool("browser_find", "Find elements matching a selector and return their
|
|
|
12712
13542
|
return err(e);
|
|
12713
13543
|
}
|
|
12714
13544
|
});
|
|
12715
|
-
server.tool("browser_snapshot", "Get
|
|
13545
|
+
server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@e0, @e1...). Use compact=true (default) for token-efficient output. Use refs in browser_click, browser_type, etc.", {
|
|
13546
|
+
session_id: exports_external.string(),
|
|
13547
|
+
compact: exports_external.boolean().optional().default(true),
|
|
13548
|
+
max_refs: exports_external.number().optional().default(50),
|
|
13549
|
+
full_tree: exports_external.boolean().optional().default(false)
|
|
13550
|
+
}, async ({ session_id, compact, max_refs, full_tree }) => {
|
|
12716
13551
|
try {
|
|
12717
13552
|
const page = getSessionPage(session_id);
|
|
12718
|
-
|
|
13553
|
+
const result = await takeSnapshot(page, session_id);
|
|
13554
|
+
setLastSnapshot(session_id, result);
|
|
13555
|
+
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
13556
|
+
const limitedRefs = Object.fromEntries(refEntries);
|
|
13557
|
+
const truncated = Object.keys(result.refs).length > max_refs;
|
|
13558
|
+
if (compact && !full_tree) {
|
|
13559
|
+
const compactRefs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 60)} [${ref}]${info.checked !== undefined ? ` checked=${info.checked}` : ""}${!info.enabled ? " disabled" : ""}`).join(`
|
|
13560
|
+
`);
|
|
13561
|
+
return json({
|
|
13562
|
+
snapshot_compact: compactRefs,
|
|
13563
|
+
interactive_count: result.interactive_count,
|
|
13564
|
+
shown_count: refEntries.length,
|
|
13565
|
+
truncated,
|
|
13566
|
+
refs: limitedRefs
|
|
13567
|
+
});
|
|
13568
|
+
}
|
|
13569
|
+
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
13570
|
+
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
13571
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
12719
13572
|
} catch (e) {
|
|
12720
13573
|
return err(e);
|
|
12721
13574
|
}
|
|
12722
13575
|
});
|
|
12723
|
-
server.tool("browser_screenshot", "Take a screenshot
|
|
13576
|
+
server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
|
|
12724
13577
|
session_id: exports_external.string(),
|
|
12725
13578
|
selector: exports_external.string().optional(),
|
|
12726
13579
|
full_page: exports_external.boolean().optional().default(false),
|
|
@@ -12728,17 +13581,37 @@ server.tool("browser_screenshot", "Take a screenshot of the page or an element",
|
|
|
12728
13581
|
quality: exports_external.number().optional(),
|
|
12729
13582
|
max_width: exports_external.number().optional().default(1280),
|
|
12730
13583
|
compress: exports_external.boolean().optional().default(true),
|
|
12731
|
-
thumbnail: exports_external.boolean().optional().default(true)
|
|
12732
|
-
|
|
13584
|
+
thumbnail: exports_external.boolean().optional().default(true),
|
|
13585
|
+
annotate: exports_external.boolean().optional().default(false)
|
|
13586
|
+
}, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
|
|
12733
13587
|
try {
|
|
12734
13588
|
const page = getSessionPage(session_id);
|
|
13589
|
+
if (annotate && !selector && !full_page) {
|
|
13590
|
+
const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
|
|
13591
|
+
const annotated = await annotateScreenshot2(page, session_id);
|
|
13592
|
+
const base64 = annotated.buffer.toString("base64");
|
|
13593
|
+
return json({
|
|
13594
|
+
base64: base64.length > 50000 ? undefined : base64,
|
|
13595
|
+
base64_truncated: base64.length > 50000,
|
|
13596
|
+
size_bytes: annotated.buffer.length,
|
|
13597
|
+
annotations: annotated.annotations,
|
|
13598
|
+
label_to_ref: annotated.labelToRef,
|
|
13599
|
+
annotation_count: annotated.annotations.length
|
|
13600
|
+
});
|
|
13601
|
+
}
|
|
12735
13602
|
const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
|
|
13603
|
+
result.url = page.url();
|
|
12736
13604
|
try {
|
|
12737
13605
|
const buf = Buffer.from(result.base64, "base64");
|
|
12738
13606
|
const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
|
|
12739
13607
|
const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
|
|
12740
13608
|
result.download_id = dl.id;
|
|
12741
13609
|
} catch {}
|
|
13610
|
+
if (result.base64.length > 50000) {
|
|
13611
|
+
result.base64_truncated = true;
|
|
13612
|
+
result.full_image_path = result.path;
|
|
13613
|
+
result.base64 = result.thumbnail_base64 ?? "";
|
|
13614
|
+
}
|
|
12742
13615
|
return json(result);
|
|
12743
13616
|
} catch (e) {
|
|
12744
13617
|
return err(e);
|
|
@@ -13033,6 +13906,37 @@ server.tool("browser_project_list", "List all registered projects", {}, async ()
|
|
|
13033
13906
|
return err(e);
|
|
13034
13907
|
}
|
|
13035
13908
|
});
|
|
13909
|
+
server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
|
|
13910
|
+
try {
|
|
13911
|
+
const page = getSessionPage(session_id);
|
|
13912
|
+
await scroll(page, direction, amount);
|
|
13913
|
+
await new Promise((r) => setTimeout(r, wait_ms));
|
|
13914
|
+
const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
|
|
13915
|
+
result.url = page.url();
|
|
13916
|
+
if (result.base64.length > 50000) {
|
|
13917
|
+
result.base64_truncated = true;
|
|
13918
|
+
result.full_image_path = result.path;
|
|
13919
|
+
result.base64 = result.thumbnail_base64 ?? "";
|
|
13920
|
+
}
|
|
13921
|
+
return json({ scrolled: { direction, amount }, screenshot: result });
|
|
13922
|
+
} catch (e) {
|
|
13923
|
+
return err(e);
|
|
13924
|
+
}
|
|
13925
|
+
});
|
|
13926
|
+
server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
|
|
13927
|
+
try {
|
|
13928
|
+
const page = getSessionPage(session_id);
|
|
13929
|
+
const start = Date.now();
|
|
13930
|
+
if (url_pattern) {
|
|
13931
|
+
await page.waitForURL(url_pattern, { timeout });
|
|
13932
|
+
} else {
|
|
13933
|
+
await page.waitForLoadState("domcontentloaded", { timeout });
|
|
13934
|
+
}
|
|
13935
|
+
return json({ url: page.url(), title: await getTitle(page), elapsed_ms: Date.now() - start });
|
|
13936
|
+
} catch (e) {
|
|
13937
|
+
return err(e);
|
|
13938
|
+
}
|
|
13939
|
+
});
|
|
13036
13940
|
server.tool("browser_session_get_by_name", "Get a session by its name", { name: exports_external.string() }, async ({ name }) => {
|
|
13037
13941
|
try {
|
|
13038
13942
|
const session = getSessionByName2(name);
|
|
@@ -13294,5 +14198,380 @@ server.tool("browser_persist_file", "Persist a file permanently via open-files S
|
|
|
13294
14198
|
return err(e);
|
|
13295
14199
|
}
|
|
13296
14200
|
});
|
|
14201
|
+
server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
14202
|
+
try {
|
|
14203
|
+
const page = getSessionPage(session_id);
|
|
14204
|
+
const before = getLastSnapshot(session_id);
|
|
14205
|
+
const after = await takeSnapshot(page, session_id);
|
|
14206
|
+
setLastSnapshot(session_id, after);
|
|
14207
|
+
if (!before) {
|
|
14208
|
+
return json({
|
|
14209
|
+
message: "No previous snapshot \u2014 returning current snapshot only.",
|
|
14210
|
+
snapshot: after.tree,
|
|
14211
|
+
refs: after.refs,
|
|
14212
|
+
interactive_count: after.interactive_count
|
|
14213
|
+
});
|
|
14214
|
+
}
|
|
14215
|
+
const diff = diffSnapshots(before, after);
|
|
14216
|
+
return json({
|
|
14217
|
+
diff,
|
|
14218
|
+
added_count: diff.added.length,
|
|
14219
|
+
removed_count: diff.removed.length,
|
|
14220
|
+
modified_count: diff.modified.length,
|
|
14221
|
+
url_changed: diff.url_changed,
|
|
14222
|
+
title_changed: diff.title_changed,
|
|
14223
|
+
current_interactive_count: after.interactive_count
|
|
14224
|
+
});
|
|
14225
|
+
} catch (e) {
|
|
14226
|
+
return err(e);
|
|
14227
|
+
}
|
|
14228
|
+
});
|
|
14229
|
+
server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
14230
|
+
try {
|
|
14231
|
+
const session = getSession2(session_id);
|
|
14232
|
+
const networkLog = getNetworkLog(session_id);
|
|
14233
|
+
const consoleLog = getConsoleLog(session_id);
|
|
14234
|
+
const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
|
|
14235
|
+
let totalChars = 0;
|
|
14236
|
+
for (const req of networkLog) {
|
|
14237
|
+
totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
|
|
14238
|
+
}
|
|
14239
|
+
for (const msg of consoleLog) {
|
|
14240
|
+
totalChars += (msg.message?.length ?? 0) + (msg.source?.length ?? 0);
|
|
14241
|
+
}
|
|
14242
|
+
for (const entry of galleryEntries) {
|
|
14243
|
+
totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
|
|
14244
|
+
}
|
|
14245
|
+
const estimatedTokens = Math.ceil(totalChars / 4);
|
|
14246
|
+
const tokenBudget = getTokenBudget(session_id);
|
|
14247
|
+
return json({
|
|
14248
|
+
session,
|
|
14249
|
+
network_request_count: networkLog.length,
|
|
14250
|
+
console_message_count: consoleLog.length,
|
|
14251
|
+
gallery_entry_count: galleryEntries.length,
|
|
14252
|
+
estimated_tokens_used: estimatedTokens,
|
|
14253
|
+
token_budget: tokenBudget,
|
|
14254
|
+
data_size_chars: totalChars
|
|
14255
|
+
});
|
|
14256
|
+
} catch (e) {
|
|
14257
|
+
return err(e);
|
|
14258
|
+
}
|
|
14259
|
+
});
|
|
14260
|
+
server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
|
|
14261
|
+
try {
|
|
14262
|
+
const page = getSessionPage(session_id);
|
|
14263
|
+
const tab = await newTab(page, url);
|
|
14264
|
+
return json(tab);
|
|
14265
|
+
} catch (e) {
|
|
14266
|
+
return err(e);
|
|
14267
|
+
}
|
|
14268
|
+
});
|
|
14269
|
+
server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
14270
|
+
try {
|
|
14271
|
+
const page = getSessionPage(session_id);
|
|
14272
|
+
const tabs = await listTabs(page);
|
|
14273
|
+
return json({ tabs, count: tabs.length });
|
|
14274
|
+
} catch (e) {
|
|
14275
|
+
return err(e);
|
|
14276
|
+
}
|
|
14277
|
+
});
|
|
14278
|
+
server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
|
|
14279
|
+
try {
|
|
14280
|
+
const page = getSessionPage(session_id);
|
|
14281
|
+
const result = await switchTab(page, tab_id);
|
|
14282
|
+
setSessionPage(session_id, result.page);
|
|
14283
|
+
return json(result.tab);
|
|
14284
|
+
} catch (e) {
|
|
14285
|
+
return err(e);
|
|
14286
|
+
}
|
|
14287
|
+
});
|
|
14288
|
+
server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
|
|
14289
|
+
try {
|
|
14290
|
+
const page = getSessionPage(session_id);
|
|
14291
|
+
const context = page.context();
|
|
14292
|
+
const result = await closeTab(page, tab_id);
|
|
14293
|
+
const remainingPages = context.pages();
|
|
14294
|
+
const newActivePage = remainingPages[result.active_tab.index];
|
|
14295
|
+
if (newActivePage) {
|
|
14296
|
+
setSessionPage(session_id, newActivePage);
|
|
14297
|
+
}
|
|
14298
|
+
return json(result);
|
|
14299
|
+
} catch (e) {
|
|
14300
|
+
return err(e);
|
|
14301
|
+
}
|
|
14302
|
+
});
|
|
14303
|
+
server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
|
|
14304
|
+
try {
|
|
14305
|
+
const result = await handleDialog(session_id, action, prompt_text);
|
|
14306
|
+
if (!result.handled)
|
|
14307
|
+
return err(new Error("No pending dialogs for this session"));
|
|
14308
|
+
return json(result);
|
|
14309
|
+
} catch (e) {
|
|
14310
|
+
return err(e);
|
|
14311
|
+
}
|
|
14312
|
+
});
|
|
14313
|
+
server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
14314
|
+
try {
|
|
14315
|
+
const dialogs = getDialogs(session_id);
|
|
14316
|
+
return json({ dialogs, count: dialogs.length });
|
|
14317
|
+
} catch (e) {
|
|
14318
|
+
return err(e);
|
|
14319
|
+
}
|
|
14320
|
+
});
|
|
14321
|
+
server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
|
|
14322
|
+
try {
|
|
14323
|
+
const page = getSessionPage(session_id);
|
|
14324
|
+
const info = await saveProfile(page, name);
|
|
14325
|
+
return json(info);
|
|
14326
|
+
} catch (e) {
|
|
14327
|
+
return err(e);
|
|
14328
|
+
}
|
|
14329
|
+
});
|
|
14330
|
+
server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
|
|
14331
|
+
try {
|
|
14332
|
+
const profileData = loadProfile(name);
|
|
14333
|
+
if (session_id) {
|
|
14334
|
+
const page = getSessionPage(session_id);
|
|
14335
|
+
const applied = await applyProfile(page, profileData);
|
|
14336
|
+
return json({ ...applied, profile: name });
|
|
14337
|
+
}
|
|
14338
|
+
return json({ profile: name, cookies: profileData.cookies.length, storage_keys: Object.keys(profileData.localStorage).length });
|
|
14339
|
+
} catch (e) {
|
|
14340
|
+
return err(e);
|
|
14341
|
+
}
|
|
14342
|
+
});
|
|
14343
|
+
server.tool("browser_profile_list", "List all saved browser profiles", {}, async () => {
|
|
14344
|
+
try {
|
|
14345
|
+
return json({ profiles: listProfiles() });
|
|
14346
|
+
} catch (e) {
|
|
14347
|
+
return err(e);
|
|
14348
|
+
}
|
|
14349
|
+
});
|
|
14350
|
+
server.tool("browser_profile_delete", "Delete a saved browser profile", { name: exports_external.string() }, async ({ name }) => {
|
|
14351
|
+
try {
|
|
14352
|
+
const deleted = deleteProfile(name);
|
|
14353
|
+
if (!deleted)
|
|
14354
|
+
return err(new Error(`Profile not found: ${name}`));
|
|
14355
|
+
return json({ deleted: name });
|
|
14356
|
+
} catch (e) {
|
|
14357
|
+
return err(e);
|
|
14358
|
+
}
|
|
14359
|
+
});
|
|
14360
|
+
server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
|
|
14361
|
+
try {
|
|
14362
|
+
const groups = {
|
|
14363
|
+
Navigation: [
|
|
14364
|
+
{ tool: "browser_navigate", description: "Navigate to a URL" },
|
|
14365
|
+
{ tool: "browser_back", description: "Navigate back in history" },
|
|
14366
|
+
{ tool: "browser_forward", description: "Navigate forward in history" },
|
|
14367
|
+
{ tool: "browser_reload", description: "Reload the current page" },
|
|
14368
|
+
{ tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
|
|
14369
|
+
],
|
|
14370
|
+
Interaction: [
|
|
14371
|
+
{ tool: "browser_click", description: "Click element by ref or selector" },
|
|
14372
|
+
{ tool: "browser_click_text", description: "Click element by visible text" },
|
|
14373
|
+
{ tool: "browser_type", description: "Type text into an element" },
|
|
14374
|
+
{ tool: "browser_hover", description: "Hover over an element" },
|
|
14375
|
+
{ tool: "browser_scroll", description: "Scroll the page" },
|
|
14376
|
+
{ tool: "browser_select", description: "Select a dropdown option" },
|
|
14377
|
+
{ tool: "browser_toggle", description: "Check/uncheck a checkbox" },
|
|
14378
|
+
{ tool: "browser_upload", description: "Upload a file to an input" },
|
|
14379
|
+
{ tool: "browser_press_key", description: "Press a keyboard key" },
|
|
14380
|
+
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
14381
|
+
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
14382
|
+
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
14383
|
+
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
14384
|
+
],
|
|
14385
|
+
Extraction: [
|
|
14386
|
+
{ tool: "browser_get_text", description: "Get text content from page/selector" },
|
|
14387
|
+
{ tool: "browser_get_html", description: "Get HTML content from page/selector" },
|
|
14388
|
+
{ tool: "browser_get_links", description: "Get all links on the page" },
|
|
14389
|
+
{ tool: "browser_get_page_info", description: "Full page summary in one call" },
|
|
14390
|
+
{ tool: "browser_extract", description: "Extract content in various formats" },
|
|
14391
|
+
{ tool: "browser_find", description: "Find elements by selector" },
|
|
14392
|
+
{ tool: "browser_element_exists", description: "Check if a selector exists" },
|
|
14393
|
+
{ tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
|
|
14394
|
+
{ tool: "browser_evaluate", description: "Execute JavaScript in page context" }
|
|
14395
|
+
],
|
|
14396
|
+
Capture: [
|
|
14397
|
+
{ tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
|
|
14398
|
+
{ tool: "browser_pdf", description: "Generate a PDF of the page" },
|
|
14399
|
+
{ tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
|
|
14400
|
+
{ tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" }
|
|
14401
|
+
],
|
|
14402
|
+
Storage: [
|
|
14403
|
+
{ tool: "browser_cookies_get", description: "Get cookies" },
|
|
14404
|
+
{ tool: "browser_cookies_set", description: "Set a cookie" },
|
|
14405
|
+
{ tool: "browser_cookies_clear", description: "Clear cookies" },
|
|
14406
|
+
{ tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
|
|
14407
|
+
{ tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
|
|
14408
|
+
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
14409
|
+
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
14410
|
+
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
14411
|
+
{ tool: "browser_profile_delete", description: "Delete a saved profile" }
|
|
14412
|
+
],
|
|
14413
|
+
Network: [
|
|
14414
|
+
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
14415
|
+
{ tool: "browser_network_intercept", description: "Add a network interception rule" },
|
|
14416
|
+
{ tool: "browser_har_start", description: "Start HAR capture" },
|
|
14417
|
+
{ tool: "browser_har_stop", description: "Stop HAR capture and get data" }
|
|
14418
|
+
],
|
|
14419
|
+
Performance: [
|
|
14420
|
+
{ tool: "browser_performance", description: "Get performance metrics" }
|
|
14421
|
+
],
|
|
14422
|
+
Console: [
|
|
14423
|
+
{ tool: "browser_console_log", description: "Get console messages" },
|
|
14424
|
+
{ tool: "browser_has_errors", description: "Check for console errors" },
|
|
14425
|
+
{ tool: "browser_clear_errors", description: "Clear console error log" },
|
|
14426
|
+
{ tool: "browser_get_dialogs", description: "Get pending dialogs" }
|
|
14427
|
+
],
|
|
14428
|
+
Recording: [
|
|
14429
|
+
{ tool: "browser_record_start", description: "Start recording actions" },
|
|
14430
|
+
{ tool: "browser_record_step", description: "Add a step to recording" },
|
|
14431
|
+
{ tool: "browser_record_stop", description: "Stop and save recording" },
|
|
14432
|
+
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
14433
|
+
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
14434
|
+
],
|
|
14435
|
+
Crawl: [
|
|
14436
|
+
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
14437
|
+
],
|
|
14438
|
+
Agent: [
|
|
14439
|
+
{ tool: "browser_register_agent", description: "Register an agent" },
|
|
14440
|
+
{ tool: "browser_heartbeat", description: "Send agent heartbeat" },
|
|
14441
|
+
{ tool: "browser_agent_list", description: "List registered agents" }
|
|
14442
|
+
],
|
|
14443
|
+
Project: [
|
|
14444
|
+
{ tool: "browser_project_create", description: "Create or ensure a project" },
|
|
14445
|
+
{ tool: "browser_project_list", description: "List all projects" }
|
|
14446
|
+
],
|
|
14447
|
+
Gallery: [
|
|
14448
|
+
{ tool: "browser_gallery_list", description: "List screenshot gallery entries" },
|
|
14449
|
+
{ tool: "browser_gallery_get", description: "Get a gallery entry by id" },
|
|
14450
|
+
{ tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
|
|
14451
|
+
{ tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
|
|
14452
|
+
{ tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
|
|
14453
|
+
{ tool: "browser_gallery_delete", description: "Delete a gallery entry" },
|
|
14454
|
+
{ tool: "browser_gallery_search", description: "Search gallery entries" },
|
|
14455
|
+
{ tool: "browser_gallery_stats", description: "Get gallery statistics" },
|
|
14456
|
+
{ tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
|
|
14457
|
+
],
|
|
14458
|
+
Downloads: [
|
|
14459
|
+
{ tool: "browser_downloads_list", description: "List downloaded files" },
|
|
14460
|
+
{ tool: "browser_downloads_get", description: "Get a download by id" },
|
|
14461
|
+
{ tool: "browser_downloads_delete", description: "Delete a download" },
|
|
14462
|
+
{ tool: "browser_downloads_clean", description: "Clean old downloads" },
|
|
14463
|
+
{ tool: "browser_downloads_export", description: "Copy download to a path" },
|
|
14464
|
+
{ tool: "browser_persist_file", description: "Persist file permanently" }
|
|
14465
|
+
],
|
|
14466
|
+
Session: [
|
|
14467
|
+
{ tool: "browser_session_create", description: "Create a new browser session" },
|
|
14468
|
+
{ tool: "browser_session_list", description: "List all sessions" },
|
|
14469
|
+
{ tool: "browser_session_close", description: "Close a session" },
|
|
14470
|
+
{ tool: "browser_session_get_by_name", description: "Get session by name" },
|
|
14471
|
+
{ tool: "browser_session_rename", description: "Rename a session" },
|
|
14472
|
+
{ tool: "browser_session_stats", description: "Get session stats and token usage" },
|
|
14473
|
+
{ tool: "browser_tab_new", description: "Open a new tab" },
|
|
14474
|
+
{ tool: "browser_tab_list", description: "List all open tabs" },
|
|
14475
|
+
{ tool: "browser_tab_switch", description: "Switch to a tab by index" },
|
|
14476
|
+
{ tool: "browser_tab_close", description: "Close a tab by index" }
|
|
14477
|
+
],
|
|
14478
|
+
Meta: [
|
|
14479
|
+
{ tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
|
|
14480
|
+
{ tool: "browser_version", description: "Show running binary version and tool count" },
|
|
14481
|
+
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
14482
|
+
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
14483
|
+
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
14484
|
+
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
14485
|
+
{ tool: "browser_watch_stop", description: "Stop DOM watcher" }
|
|
14486
|
+
]
|
|
14487
|
+
};
|
|
14488
|
+
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
14489
|
+
return json({ groups, total_tools: totalTools });
|
|
14490
|
+
} catch (e) {
|
|
14491
|
+
return err(e);
|
|
14492
|
+
}
|
|
14493
|
+
});
|
|
14494
|
+
server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
|
|
14495
|
+
try {
|
|
14496
|
+
const { getDataDir: getDataDir4 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
14497
|
+
const toolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
14498
|
+
return json({
|
|
14499
|
+
version: _pkg.version,
|
|
14500
|
+
mcp_tools_count: toolCount,
|
|
14501
|
+
bun_version: Bun.version,
|
|
14502
|
+
data_dir: getDataDir4(),
|
|
14503
|
+
node_env: process.env["NODE_ENV"] ?? "production"
|
|
14504
|
+
});
|
|
14505
|
+
} catch (e) {
|
|
14506
|
+
return err(e);
|
|
14507
|
+
}
|
|
14508
|
+
});
|
|
14509
|
+
server.tool("browser_scroll_to_element", "Scroll an element into view (by ref or selector) then optionally take a screenshot of it. Replaces scroll + wait + screenshot pattern.", {
|
|
14510
|
+
session_id: exports_external.string(),
|
|
14511
|
+
selector: exports_external.string().optional(),
|
|
14512
|
+
ref: exports_external.string().optional(),
|
|
14513
|
+
screenshot: exports_external.boolean().optional().default(true),
|
|
14514
|
+
wait_ms: exports_external.number().optional().default(200)
|
|
14515
|
+
}, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
|
|
14516
|
+
try {
|
|
14517
|
+
const page = getSessionPage(session_id);
|
|
14518
|
+
let locator;
|
|
14519
|
+
if (ref) {
|
|
14520
|
+
const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
14521
|
+
locator = getRefLocator2(page, session_id, ref);
|
|
14522
|
+
} else if (selector) {
|
|
14523
|
+
locator = page.locator(selector).first();
|
|
14524
|
+
} else {
|
|
14525
|
+
return err(new Error("Either ref or selector is required"));
|
|
14526
|
+
}
|
|
14527
|
+
await locator.scrollIntoViewIfNeeded();
|
|
14528
|
+
await new Promise((r) => setTimeout(r, wait_ms));
|
|
14529
|
+
const result = { scrolled: ref ?? selector };
|
|
14530
|
+
if (doScreenshot) {
|
|
14531
|
+
try {
|
|
14532
|
+
const ss = await takeScreenshot(page, { selector, track: false });
|
|
14533
|
+
ss.url = page.url();
|
|
14534
|
+
if (ss.base64.length > 50000) {
|
|
14535
|
+
ss.base64_truncated = true;
|
|
14536
|
+
ss.base64 = ss.thumbnail_base64 ?? "";
|
|
14537
|
+
}
|
|
14538
|
+
result.screenshot = ss;
|
|
14539
|
+
} catch {}
|
|
14540
|
+
}
|
|
14541
|
+
return json(result);
|
|
14542
|
+
} catch (e) {
|
|
14543
|
+
return err(e);
|
|
14544
|
+
}
|
|
14545
|
+
});
|
|
14546
|
+
server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
14547
|
+
try {
|
|
14548
|
+
const page = getSessionPage(session_id);
|
|
14549
|
+
const info = await getPageInfo(page);
|
|
14550
|
+
const errors2 = getConsoleLog(session_id, "error");
|
|
14551
|
+
info.has_console_errors = errors2.length > 0;
|
|
14552
|
+
let perf = {};
|
|
14553
|
+
try {
|
|
14554
|
+
perf = await getPerformanceMetrics(page);
|
|
14555
|
+
} catch {}
|
|
14556
|
+
let thumbnail_base64 = "";
|
|
14557
|
+
try {
|
|
14558
|
+
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
14559
|
+
thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
14560
|
+
} catch {}
|
|
14561
|
+
let snapshot_refs = "";
|
|
14562
|
+
let interactive_count = 0;
|
|
14563
|
+
try {
|
|
14564
|
+
const snap = await takeSnapshot(page, session_id);
|
|
14565
|
+
setLastSnapshot(session_id, snap);
|
|
14566
|
+
interactive_count = snap.interactive_count;
|
|
14567
|
+
snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
14568
|
+
} catch {}
|
|
14569
|
+
return json({ ...info, error_count: errors2.length, performance: perf, thumbnail_base64, snapshot_refs, interactive_count });
|
|
14570
|
+
} catch (e) {
|
|
14571
|
+
return err(e);
|
|
14572
|
+
}
|
|
14573
|
+
});
|
|
14574
|
+
var _startupToolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
14575
|
+
console.error(`@hasna/browser v${_pkg.version} \u2014 ${_startupToolCount} tools | data: ${(await Promise.resolve().then(() => (init_schema(), exports_schema))).getDataDir()}`);
|
|
13297
14576
|
var transport = new StdioServerTransport;
|
|
13298
14577
|
await server.connect(transport);
|