@hasna/browser 0.0.9 → 0.1.0

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/mcp/index.js CHANGED
@@ -307,6 +307,23 @@ function runMigrations(db) {
307
307
  );
308
308
  CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
309
309
  `
310
+ },
311
+ {
312
+ version: 6,
313
+ sql: `
314
+ CREATE TABLE IF NOT EXISTS auth_flows (
315
+ id TEXT PRIMARY KEY,
316
+ name TEXT NOT NULL UNIQUE,
317
+ domain TEXT NOT NULL,
318
+ recording_id TEXT REFERENCES recordings(id),
319
+ storage_state_path TEXT,
320
+ created_at TEXT DEFAULT (datetime('now')),
321
+ last_used TEXT
322
+ );
323
+
324
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
325
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
326
+ `
310
327
  }
311
328
  ];
312
329
  for (const m of migrations) {
@@ -1418,6 +1435,188 @@ var init_dialogs = __esm(() => {
1418
1435
  pendingDialogs = new Map;
1419
1436
  });
1420
1437
 
1438
+ // src/engines/cdp.ts
1439
+ var exports_cdp = {};
1440
+ __export(exports_cdp, {
1441
+ connectToExistingBrowser: () => connectToExistingBrowser,
1442
+ CDPClient: () => CDPClient
1443
+ });
1444
+ async function connectToExistingBrowser(cdpUrl) {
1445
+ const { chromium: chromium3 } = await import("playwright");
1446
+ try {
1447
+ return await chromium3.connectOverCDP(cdpUrl);
1448
+ } catch (err) {
1449
+ throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
1450
+ }
1451
+ }
1452
+
1453
+ class CDPClient {
1454
+ session;
1455
+ networkEnabled = false;
1456
+ performanceEnabled = false;
1457
+ constructor(session) {
1458
+ this.session = session;
1459
+ }
1460
+ static async fromPage(page) {
1461
+ try {
1462
+ const session = await page.context().newCDPSession(page);
1463
+ return new CDPClient(session);
1464
+ } catch (err) {
1465
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
1466
+ }
1467
+ }
1468
+ async send(method, params) {
1469
+ try {
1470
+ return await this.session.send(method, params);
1471
+ } catch (err) {
1472
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
1473
+ }
1474
+ }
1475
+ on(event, handler) {
1476
+ this.session.on(event, handler);
1477
+ }
1478
+ off(event, handler) {
1479
+ this.session.off(event, handler);
1480
+ }
1481
+ async enableNetwork() {
1482
+ if (!this.networkEnabled) {
1483
+ await this.send("Network.enable");
1484
+ this.networkEnabled = true;
1485
+ }
1486
+ }
1487
+ async enablePerformance() {
1488
+ if (!this.performanceEnabled) {
1489
+ await this.send("Performance.enable");
1490
+ this.performanceEnabled = true;
1491
+ }
1492
+ }
1493
+ async getPerformanceMetrics() {
1494
+ await this.enablePerformance();
1495
+ const result = await this.send("Performance.getMetrics");
1496
+ const m = {};
1497
+ for (const metric of result.metrics) {
1498
+ m[metric.name] = metric.value;
1499
+ }
1500
+ return {
1501
+ js_heap_size_used: m["JSHeapUsedSize"],
1502
+ js_heap_size_total: m["JSHeapTotalSize"],
1503
+ dom_interactive: m["DOMInteractive"],
1504
+ dom_complete: m["DOMComplete"],
1505
+ load_event: m["LoadEventEnd"]
1506
+ };
1507
+ }
1508
+ async startJSCoverage() {
1509
+ await this.send("Profiler.enable");
1510
+ await this.send("Debugger.enable");
1511
+ await this.send("Profiler.startPreciseCoverage", {
1512
+ callCount: false,
1513
+ detailed: true
1514
+ });
1515
+ }
1516
+ async stopJSCoverage() {
1517
+ const result = await this.send("Profiler.takePreciseCoverage");
1518
+ await this.send("Profiler.stopPreciseCoverage");
1519
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
1520
+ url: r.url,
1521
+ text: "",
1522
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
1523
+ }));
1524
+ }
1525
+ async getCoverage() {
1526
+ await this.startJSCoverage();
1527
+ const js = await this.stopJSCoverage();
1528
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
1529
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
1530
+ }
1531
+ async captureHAREntries(page, handler) {
1532
+ await this.enableNetwork();
1533
+ const requestTimings = new Map;
1534
+ const onRequest = (params) => {
1535
+ requestTimings.set(params.requestId, params.timestamp);
1536
+ };
1537
+ const onResponse = (params) => {
1538
+ const start = requestTimings.get(params.requestId);
1539
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
1540
+ handler({
1541
+ method: "GET",
1542
+ url: params.response.url,
1543
+ status: params.response.status,
1544
+ duration
1545
+ });
1546
+ };
1547
+ this.on("Network.requestWillBeSent", onRequest);
1548
+ this.on("Network.responseReceived", onResponse);
1549
+ return () => {
1550
+ this.off("Network.requestWillBeSent", onRequest);
1551
+ this.off("Network.responseReceived", onResponse);
1552
+ };
1553
+ }
1554
+ async detach() {
1555
+ try {
1556
+ await this.session.detach();
1557
+ } catch {}
1558
+ }
1559
+ }
1560
+ var init_cdp = __esm(() => {
1561
+ init_types();
1562
+ });
1563
+
1564
+ // src/lib/storage-state.ts
1565
+ var exports_storage_state = {};
1566
+ __export(exports_storage_state, {
1567
+ saveStateFromPage: () => saveStateFromPage,
1568
+ saveState: () => saveState,
1569
+ loadStatePath: () => loadStatePath,
1570
+ listStates: () => listStates,
1571
+ deleteState: () => deleteState
1572
+ });
1573
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
1574
+ import { join as join3 } from "path";
1575
+ import { homedir as homedir3 } from "os";
1576
+ function ensureDir() {
1577
+ mkdirSync3(STATES_DIR, { recursive: true });
1578
+ }
1579
+ function statePath(name) {
1580
+ return join3(STATES_DIR, `${name}.json`);
1581
+ }
1582
+ async function saveState(context, name) {
1583
+ ensureDir();
1584
+ const path = statePath(name);
1585
+ const state = await context.storageState({ path });
1586
+ return path;
1587
+ }
1588
+ async function saveStateFromPage(page, name) {
1589
+ return saveState(page.context(), name);
1590
+ }
1591
+ function loadStatePath(name) {
1592
+ const path = statePath(name);
1593
+ return existsSync(path) ? path : null;
1594
+ }
1595
+ function listStates() {
1596
+ ensureDir();
1597
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
1598
+ const path = join3(STATES_DIR, f);
1599
+ const stat = Bun.file(path);
1600
+ return {
1601
+ name: f.replace(".json", ""),
1602
+ path,
1603
+ modified: new Date(stat.lastModified).toISOString()
1604
+ };
1605
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
1606
+ }
1607
+ function deleteState(name) {
1608
+ const path = statePath(name);
1609
+ if (existsSync(path)) {
1610
+ unlinkSync(path);
1611
+ return true;
1612
+ }
1613
+ return false;
1614
+ }
1615
+ var STATES_DIR;
1616
+ var init_storage_state = __esm(() => {
1617
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
1618
+ });
1619
+
1421
1620
  // src/lib/session.ts
1422
1621
  var exports_session = {};
1423
1622
  __export(exports_session, {
@@ -1447,6 +1646,37 @@ function createBunProxy(view) {
1447
1646
  return view;
1448
1647
  }
1449
1648
  async function createSession2(opts = {}) {
1649
+ if (opts.cdpUrl) {
1650
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
1651
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
1652
+ const contexts = cdpBrowser.contexts();
1653
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
1654
+ const pages = context.pages();
1655
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
1656
+ const session2 = createSession({
1657
+ engine: "cdp",
1658
+ projectId: opts.projectId,
1659
+ agentId: opts.agentId,
1660
+ startUrl: page2.url(),
1661
+ name: opts.name ?? "attached"
1662
+ });
1663
+ const cleanups2 = [];
1664
+ if (opts.captureNetwork !== false) {
1665
+ try {
1666
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
1667
+ } catch {}
1668
+ }
1669
+ if (opts.captureConsole !== false) {
1670
+ try {
1671
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
1672
+ } catch {}
1673
+ }
1674
+ try {
1675
+ cleanups2.push(setupDialogHandler(page2, session2.id));
1676
+ } catch {}
1677
+ handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
1678
+ return { session: session2, page: page2 };
1679
+ }
1450
1680
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
1451
1681
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
1452
1682
  let browser = null;
@@ -1472,7 +1702,22 @@ async function createSession2(opts = {}) {
1472
1702
  page = await context.newPage();
1473
1703
  } else {
1474
1704
  browser = await pool.acquire(opts.headless ?? true);
1475
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1705
+ if (opts.storageState) {
1706
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
1707
+ const statePath2 = loadStatePath2(opts.storageState);
1708
+ if (statePath2) {
1709
+ const context = await browser.newContext({
1710
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
1711
+ userAgent: opts.userAgent,
1712
+ storageState: statePath2
1713
+ });
1714
+ page = await context.newPage();
1715
+ } else {
1716
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1717
+ }
1718
+ } else {
1719
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1720
+ }
1476
1721
  }
1477
1722
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
1478
1723
  try {
@@ -1977,6 +2222,66 @@ var init_snapshot = __esm(() => {
1977
2222
  ];
1978
2223
  });
1979
2224
 
2225
+ // src/lib/self-heal.ts
2226
+ async function healSelector(page, selector, sessionId) {
2227
+ const attempts = [];
2228
+ attempts.push(`selector: ${selector}`);
2229
+ try {
2230
+ const loc = page.locator(selector).first();
2231
+ if (await loc.count() > 0) {
2232
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
2233
+ }
2234
+ } catch {}
2235
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
2236
+ attempts.push(`text: "${selector}"`);
2237
+ try {
2238
+ const loc = page.getByText(selector, { exact: false }).first();
2239
+ if (await loc.count() > 0) {
2240
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
2241
+ }
2242
+ } catch {}
2243
+ }
2244
+ const roleMap = {
2245
+ button: ["button", "submit", "reset"],
2246
+ link: ["a"],
2247
+ input: ["input", "textarea"],
2248
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
2249
+ };
2250
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
2251
+ for (const [role, tags] of Object.entries(roleMap)) {
2252
+ attempts.push(`role: ${role} name~="${nameHint}"`);
2253
+ try {
2254
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
2255
+ if (await loc.count() > 0) {
2256
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
2257
+ }
2258
+ } catch {}
2259
+ }
2260
+ if (selector.startsWith("#")) {
2261
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
2262
+ const partialSel = `[id*="${idPart}"]`;
2263
+ attempts.push(`partial_id: ${partialSel}`);
2264
+ try {
2265
+ const loc = page.locator(partialSel).first();
2266
+ if (await loc.count() > 0) {
2267
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
2268
+ }
2269
+ } catch {}
2270
+ }
2271
+ if (selector.startsWith(".")) {
2272
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
2273
+ const partialSel = `[class*="${classPart}"]`;
2274
+ attempts.push(`partial_class: ${partialSel}`);
2275
+ try {
2276
+ const loc = page.locator(partialSel).first();
2277
+ if (await loc.count() > 0) {
2278
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
2279
+ }
2280
+ } catch {}
2281
+ }
2282
+ return { found: false, locator: null, method: "none", healed: false, attempts };
2283
+ }
2284
+
1980
2285
  // src/lib/actions.ts
1981
2286
  var exports_actions = {};
1982
2287
  __export(exports_actions, {
@@ -2018,11 +2323,22 @@ async function click(page, selector, opts) {
2018
2323
  delay: opts?.delay,
2019
2324
  timeout: opts?.timeout ?? 1e4
2020
2325
  });
2021
- } catch (err) {
2022
- if (err instanceof Error && err.message.includes("not found")) {
2326
+ return {};
2327
+ } catch (originalError) {
2328
+ if (opts?.selfHeal !== false) {
2329
+ const result = await healSelector(page, selector);
2330
+ if (result.found && result.locator) {
2331
+ await result.locator.click({
2332
+ button: opts?.button ?? "left",
2333
+ timeout: opts?.timeout ?? 1e4
2334
+ });
2335
+ return { healed: true, method: result.method, attempts: result.attempts };
2336
+ }
2337
+ }
2338
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
2023
2339
  throw new ElementNotFoundError(selector);
2024
2340
  }
2025
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
2341
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
2026
2342
  }
2027
2343
  }
2028
2344
  async function type(page, selector, text, opts) {
@@ -2031,17 +2347,35 @@ async function type(page, selector, text, opts) {
2031
2347
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
2032
2348
  }
2033
2349
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
2034
- } catch (err) {
2035
- if (err instanceof Error && err.message.includes("not found")) {
2350
+ return {};
2351
+ } catch (originalError) {
2352
+ if (opts?.selfHeal !== false) {
2353
+ const result = await healSelector(page, selector);
2354
+ if (result.found && result.locator) {
2355
+ if (opts?.clear)
2356
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
2357
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
2358
+ return { healed: true, method: result.method, attempts: result.attempts };
2359
+ }
2360
+ }
2361
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
2036
2362
  throw new ElementNotFoundError(selector);
2037
2363
  }
2038
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
2364
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
2039
2365
  }
2040
2366
  }
2041
- async function fill(page, selector, value, timeout = 1e4) {
2367
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
2042
2368
  try {
2043
2369
  await page.fill(selector, value, { timeout });
2044
- } catch (err) {
2370
+ return {};
2371
+ } catch (originalError) {
2372
+ if (selfHeal) {
2373
+ const result = await healSelector(page, selector);
2374
+ if (result.found && result.locator) {
2375
+ await result.locator.fill(value, { timeout });
2376
+ return { healed: true, method: result.method, attempts: result.attempts };
2377
+ }
2378
+ }
2045
2379
  throw new ElementNotFoundError(selector);
2046
2380
  }
2047
2381
  }
@@ -2166,12 +2500,39 @@ async function clickText(page, text, opts) {
2166
2500
  }
2167
2501
  }, { retries: opts?.retries ?? 1 });
2168
2502
  }
2169
- async function fillForm(page, fields, submitSelector) {
2503
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
2170
2504
  let filled = 0;
2171
2505
  const errors2 = [];
2506
+ const healedFields = [];
2172
2507
  for (const [selector, value] of Object.entries(fields)) {
2173
2508
  try {
2174
- const el = await page.$(selector);
2509
+ let el = await page.$(selector);
2510
+ if (!el && selfHeal) {
2511
+ const result = await healSelector(page, selector);
2512
+ if (result.found && result.locator) {
2513
+ const handle = await result.locator.elementHandle();
2514
+ if (handle) {
2515
+ el = handle;
2516
+ healedFields.push(selector);
2517
+ const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
2518
+ const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
2519
+ if (tagName2 === "select") {
2520
+ await result.locator.selectOption(String(value));
2521
+ } else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
2522
+ if (Boolean(value))
2523
+ await result.locator.check();
2524
+ else
2525
+ await result.locator.uncheck();
2526
+ } else {
2527
+ await result.locator.fill(String(value));
2528
+ }
2529
+ filled++;
2530
+ continue;
2531
+ }
2532
+ }
2533
+ errors2.push(`${selector}: element not found`);
2534
+ continue;
2535
+ }
2175
2536
  if (!el) {
2176
2537
  errors2.push(`${selector}: element not found`);
2177
2538
  continue;
@@ -2198,11 +2559,21 @@ async function fillForm(page, fields, submitSelector) {
2198
2559
  if (submitSelector) {
2199
2560
  try {
2200
2561
  await page.click(submitSelector);
2201
- } catch (err) {
2202
- errors2.push(`submit(${submitSelector}): ${err instanceof Error ? err.message : String(err)}`);
2562
+ } catch (submitErr) {
2563
+ if (selfHeal) {
2564
+ const result = await healSelector(page, submitSelector);
2565
+ if (result.found && result.locator) {
2566
+ await result.locator.click();
2567
+ healedFields.push(submitSelector);
2568
+ } else {
2569
+ errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
2570
+ }
2571
+ } else {
2572
+ errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
2573
+ }
2203
2574
  }
2204
2575
  }
2205
- return { filled, errors: errors2, fields_attempted: Object.keys(fields).length };
2576
+ return { filled, errors: errors2, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
2206
2577
  }
2207
2578
  async function waitForText(page, text, opts) {
2208
2579
  const timeout = opts?.timeout ?? 1e4;
@@ -9057,6 +9428,237 @@ var init_gallery = __esm(() => {
9057
9428
  init_schema();
9058
9429
  });
9059
9430
 
9431
+ // src/db/recordings.ts
9432
+ import { randomUUID as randomUUID5 } from "crypto";
9433
+ function deserialize2(row) {
9434
+ return {
9435
+ ...row,
9436
+ project_id: row.project_id ?? undefined,
9437
+ start_url: row.start_url ?? undefined,
9438
+ steps: JSON.parse(row.steps)
9439
+ };
9440
+ }
9441
+ function createRecording(data) {
9442
+ const db = getDatabase();
9443
+ const id = randomUUID5();
9444
+ db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
9445
+ return getRecording(id);
9446
+ }
9447
+ function getRecording(id) {
9448
+ const db = getDatabase();
9449
+ const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
9450
+ if (!row)
9451
+ throw new RecordingNotFoundError(id);
9452
+ return deserialize2(row);
9453
+ }
9454
+ function listRecordings(projectId) {
9455
+ const db = getDatabase();
9456
+ const rows = projectId ? db.query("SELECT * FROM recordings WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM recordings ORDER BY created_at DESC").all();
9457
+ return rows.map(deserialize2);
9458
+ }
9459
+ function updateRecording(id, data) {
9460
+ const db = getDatabase();
9461
+ const fields = [];
9462
+ const values = [];
9463
+ if (data.name !== undefined) {
9464
+ fields.push("name = ?");
9465
+ values.push(data.name);
9466
+ }
9467
+ if (data.steps !== undefined) {
9468
+ fields.push("steps = ?");
9469
+ values.push(JSON.stringify(data.steps));
9470
+ }
9471
+ if (data.start_url !== undefined) {
9472
+ fields.push("start_url = ?");
9473
+ values.push(data.start_url ?? null);
9474
+ }
9475
+ if (fields.length === 0)
9476
+ return getRecording(id);
9477
+ values.push(id);
9478
+ db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
9479
+ return getRecording(id);
9480
+ }
9481
+ var init_recordings = __esm(() => {
9482
+ init_schema();
9483
+ init_types();
9484
+ });
9485
+
9486
+ // src/lib/recorder.ts
9487
+ var exports_recorder = {};
9488
+ __export(exports_recorder, {
9489
+ stopRecording: () => stopRecording,
9490
+ startRecording: () => startRecording,
9491
+ replayRecording: () => replayRecording,
9492
+ recordStep: () => recordStep,
9493
+ listRecordings: () => listRecordings,
9494
+ getRecording: () => getRecording,
9495
+ exportRecording: () => exportRecording,
9496
+ attachPageListeners: () => attachPageListeners
9497
+ });
9498
+ function startRecording(sessionId, name, startUrl) {
9499
+ const steps = [];
9500
+ const recording = createRecording({ name, start_url: startUrl, steps });
9501
+ activeRecordings.set(recording.id, {
9502
+ id: recording.id,
9503
+ steps,
9504
+ cleanup: () => {}
9505
+ });
9506
+ return recording;
9507
+ }
9508
+ function attachPageListeners(page, recordingId) {
9509
+ const active = activeRecordings.get(recordingId);
9510
+ if (!active)
9511
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9512
+ const onFrameNav = () => {
9513
+ active.steps.push({
9514
+ type: "navigate",
9515
+ url: page.url(),
9516
+ timestamp: Date.now()
9517
+ });
9518
+ };
9519
+ page.on("framenavigated", onFrameNav);
9520
+ const cleanup = () => {
9521
+ page.off("framenavigated", onFrameNav);
9522
+ };
9523
+ active.cleanup = cleanup;
9524
+ }
9525
+ function recordStep(recordingId, step) {
9526
+ const active = activeRecordings.get(recordingId);
9527
+ if (!active)
9528
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9529
+ active.steps.push({ ...step, timestamp: Date.now() });
9530
+ }
9531
+ function stopRecording(recordingId) {
9532
+ const active = activeRecordings.get(recordingId);
9533
+ if (!active)
9534
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9535
+ active.cleanup();
9536
+ activeRecordings.delete(recordingId);
9537
+ return updateRecording(recordingId, { steps: active.steps });
9538
+ }
9539
+ async function replayRecording(recordingId, page) {
9540
+ const recording = getRecording(recordingId);
9541
+ const startTime = Date.now();
9542
+ let executed = 0;
9543
+ let failed = 0;
9544
+ const errors2 = [];
9545
+ for (const step of recording.steps) {
9546
+ try {
9547
+ switch (step.type) {
9548
+ case "navigate":
9549
+ if (step.url)
9550
+ await navigate(page, step.url);
9551
+ break;
9552
+ case "click":
9553
+ if (step.selector)
9554
+ await click(page, step.selector);
9555
+ break;
9556
+ case "type":
9557
+ if (step.selector && step.value)
9558
+ await type(page, step.selector, step.value);
9559
+ break;
9560
+ case "scroll":
9561
+ await scroll(page, "down");
9562
+ break;
9563
+ case "hover":
9564
+ if (step.selector) {
9565
+ const el = await page.$(step.selector);
9566
+ if (el)
9567
+ await el.hover();
9568
+ }
9569
+ break;
9570
+ case "evaluate":
9571
+ if (step.value)
9572
+ await page.evaluate(step.value);
9573
+ break;
9574
+ case "wait":
9575
+ if (step.selector) {
9576
+ await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
9577
+ }
9578
+ break;
9579
+ }
9580
+ executed++;
9581
+ } catch (err) {
9582
+ failed++;
9583
+ errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
9584
+ }
9585
+ await new Promise((r) => setTimeout(r, 100));
9586
+ }
9587
+ return {
9588
+ recording_id: recordingId,
9589
+ success: failed === 0,
9590
+ steps_executed: executed,
9591
+ steps_failed: failed,
9592
+ errors: errors2,
9593
+ duration_ms: Date.now() - startTime
9594
+ };
9595
+ }
9596
+ function exportRecording(recordingId, format = "json") {
9597
+ const recording = getRecording(recordingId);
9598
+ if (format === "json") {
9599
+ return JSON.stringify(recording, null, 2);
9600
+ }
9601
+ if (format === "playwright") {
9602
+ const lines2 = [
9603
+ `import { test, expect } from '@playwright/test';`,
9604
+ ``,
9605
+ `test('${recording.name}', async ({ page }) => {`
9606
+ ];
9607
+ for (const step of recording.steps) {
9608
+ switch (step.type) {
9609
+ case "navigate":
9610
+ lines2.push(` await page.goto('${step.url}');`);
9611
+ break;
9612
+ case "click":
9613
+ lines2.push(` await page.click('${step.selector}');`);
9614
+ break;
9615
+ case "type":
9616
+ lines2.push(` await page.type('${step.selector}', '${step.value}');`);
9617
+ break;
9618
+ case "scroll":
9619
+ lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
9620
+ break;
9621
+ case "evaluate":
9622
+ lines2.push(` await page.evaluate(${step.value});`);
9623
+ break;
9624
+ }
9625
+ }
9626
+ lines2.push(`});`);
9627
+ return lines2.join(`
9628
+ `);
9629
+ }
9630
+ const lines = [
9631
+ `const puppeteer = require('puppeteer');`,
9632
+ ``,
9633
+ `(async () => {`,
9634
+ ` const browser = await puppeteer.launch();`,
9635
+ ` const page = await browser.newPage();`
9636
+ ];
9637
+ for (const step of recording.steps) {
9638
+ switch (step.type) {
9639
+ case "navigate":
9640
+ lines.push(` await page.goto('${step.url}');`);
9641
+ break;
9642
+ case "click":
9643
+ lines.push(` await page.click('${step.selector}');`);
9644
+ break;
9645
+ case "type":
9646
+ lines.push(` await page.type('${step.selector}', '${step.value}');`);
9647
+ break;
9648
+ }
9649
+ }
9650
+ lines.push(` await browser.close();`, `})();`);
9651
+ return lines.join(`
9652
+ `);
9653
+ }
9654
+ var activeRecordings;
9655
+ var init_recorder = __esm(() => {
9656
+ init_recordings();
9657
+ init_actions();
9658
+ init_types();
9659
+ activeRecordings = new Map;
9660
+ });
9661
+
9060
9662
  // src/lib/profiles.ts
9061
9663
  var exports_profiles = {};
9062
9664
  __export(exports_profiles, {
@@ -9066,23 +9668,23 @@ __export(exports_profiles, {
9066
9668
  deleteProfile: () => deleteProfile,
9067
9669
  applyProfile: () => applyProfile
9068
9670
  });
9069
- import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
9070
- import { join as join7 } from "path";
9071
- import { homedir as homedir7 } from "os";
9671
+ import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
9672
+ import { join as join8 } from "path";
9673
+ import { homedir as homedir8 } from "os";
9072
9674
  function getProfilesDir() {
9073
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
9074
- const dir = join7(dataDir, "profiles");
9075
- mkdirSync7(dir, { recursive: true });
9675
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
9676
+ const dir = join8(dataDir, "profiles");
9677
+ mkdirSync8(dir, { recursive: true });
9076
9678
  return dir;
9077
9679
  }
9078
9680
  function getProfileDir2(name) {
9079
- return join7(getProfilesDir(), name);
9681
+ return join8(getProfilesDir(), name);
9080
9682
  }
9081
9683
  async function saveProfile(page, name) {
9082
9684
  const dir = getProfileDir2(name);
9083
- mkdirSync7(dir, { recursive: true });
9685
+ mkdirSync8(dir, { recursive: true });
9084
9686
  const cookies = await page.context().cookies();
9085
- writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
9687
+ writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
9086
9688
  let localStorage2 = {};
9087
9689
  try {
9088
9690
  localStorage2 = await page.evaluate(() => {
@@ -9094,11 +9696,11 @@ async function saveProfile(page, name) {
9094
9696
  return result;
9095
9697
  });
9096
9698
  } catch {}
9097
- writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
9699
+ writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
9098
9700
  const savedAt = new Date().toISOString();
9099
9701
  const url = page.url();
9100
9702
  const meta = { saved_at: savedAt, url };
9101
- writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
9703
+ writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
9102
9704
  return {
9103
9705
  name,
9104
9706
  saved_at: savedAt,
@@ -9109,17 +9711,17 @@ async function saveProfile(page, name) {
9109
9711
  }
9110
9712
  function loadProfile(name) {
9111
9713
  const dir = getProfileDir2(name);
9112
- if (!existsSync3(dir)) {
9714
+ if (!existsSync4(dir)) {
9113
9715
  throw new Error(`Profile not found: ${name}`);
9114
9716
  }
9115
- const cookiesPath = join7(dir, "cookies.json");
9116
- const storagePath = join7(dir, "storage.json");
9117
- const metaPath2 = join7(dir, "meta.json");
9118
- const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
9119
- const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
9717
+ const cookiesPath = join8(dir, "cookies.json");
9718
+ const storagePath = join8(dir, "storage.json");
9719
+ const metaPath2 = join8(dir, "meta.json");
9720
+ const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
9721
+ const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
9120
9722
  let savedAt = new Date().toISOString();
9121
9723
  let url;
9122
- if (existsSync3(metaPath2)) {
9724
+ if (existsSync4(metaPath2)) {
9123
9725
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
9124
9726
  savedAt = meta.saved_at ?? savedAt;
9125
9727
  url = meta.url;
@@ -9147,33 +9749,33 @@ async function applyProfile(page, profileData) {
9147
9749
  }
9148
9750
  function listProfiles() {
9149
9751
  const dir = getProfilesDir();
9150
- if (!existsSync3(dir))
9752
+ if (!existsSync4(dir))
9151
9753
  return [];
9152
- const entries = readdirSync2(dir, { withFileTypes: true });
9754
+ const entries = readdirSync3(dir, { withFileTypes: true });
9153
9755
  const profiles = [];
9154
9756
  for (const entry of entries) {
9155
9757
  if (!entry.isDirectory())
9156
9758
  continue;
9157
9759
  const name = entry.name;
9158
- const profileDir = join7(dir, name);
9760
+ const profileDir = join8(dir, name);
9159
9761
  let savedAt = "";
9160
9762
  let url;
9161
9763
  let cookieCount = 0;
9162
9764
  let storageKeyCount = 0;
9163
9765
  try {
9164
- const metaPath2 = join7(profileDir, "meta.json");
9165
- if (existsSync3(metaPath2)) {
9766
+ const metaPath2 = join8(profileDir, "meta.json");
9767
+ if (existsSync4(metaPath2)) {
9166
9768
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
9167
9769
  savedAt = meta.saved_at ?? "";
9168
9770
  url = meta.url;
9169
9771
  }
9170
- const cookiesPath = join7(profileDir, "cookies.json");
9171
- if (existsSync3(cookiesPath)) {
9772
+ const cookiesPath = join8(profileDir, "cookies.json");
9773
+ if (existsSync4(cookiesPath)) {
9172
9774
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
9173
9775
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
9174
9776
  }
9175
- const storagePath = join7(profileDir, "storage.json");
9176
- if (existsSync3(storagePath)) {
9777
+ const storagePath = join8(profileDir, "storage.json");
9778
+ if (existsSync4(storagePath)) {
9177
9779
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
9178
9780
  storageKeyCount = Object.keys(storage).length;
9179
9781
  }
@@ -9190,7 +9792,7 @@ function listProfiles() {
9190
9792
  }
9191
9793
  function deleteProfile(name) {
9192
9794
  const dir = getProfileDir2(name);
9193
- if (!existsSync3(dir))
9795
+ if (!existsSync4(dir))
9194
9796
  return false;
9195
9797
  try {
9196
9798
  rmSync(dir, { recursive: true, force: true });
@@ -9201,6 +9803,97 @@ function deleteProfile(name) {
9201
9803
  }
9202
9804
  var init_profiles = () => {};
9203
9805
 
9806
+ // src/lib/sanitize.ts
9807
+ var exports_sanitize = {};
9808
+ __export(exports_sanitize, {
9809
+ sanitizeText: () => sanitizeText,
9810
+ sanitizeHTML: () => sanitizeHTML
9811
+ });
9812
+ function sanitizeText(text) {
9813
+ let stripped = 0;
9814
+ const warnings = [];
9815
+ let clean = text;
9816
+ for (const pattern of INJECTION_PATTERNS) {
9817
+ pattern.lastIndex = 0;
9818
+ const matches = clean.match(pattern);
9819
+ if (matches) {
9820
+ stripped += matches.length;
9821
+ warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
9822
+ pattern.lastIndex = 0;
9823
+ clean = clean.replace(pattern, "[STRIPPED]");
9824
+ }
9825
+ }
9826
+ return { text: clean, stripped, warnings };
9827
+ }
9828
+ function sanitizeHTML(html) {
9829
+ let stripped = 0;
9830
+ const warnings = [];
9831
+ let clean = html;
9832
+ const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
9833
+ if (commentMatches) {
9834
+ for (const comment of commentMatches) {
9835
+ if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
9836
+ stripped++;
9837
+ warnings.push(`Stripped HTML comment (${comment.length} chars)`);
9838
+ }
9839
+ }
9840
+ clean = clean.replace(/<!--[\s\S]*?-->/g, "");
9841
+ }
9842
+ const hiddenPatterns = [
9843
+ /style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
9844
+ /style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
9845
+ /style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
9846
+ /style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
9847
+ /style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
9848
+ ];
9849
+ for (const pattern of hiddenPatterns) {
9850
+ pattern.lastIndex = 0;
9851
+ const matches = clean.match(pattern);
9852
+ if (matches) {
9853
+ stripped += matches.length;
9854
+ warnings.push(`Stripped ${matches.length} hidden elements`);
9855
+ pattern.lastIndex = 0;
9856
+ clean = clean.replace(pattern, "");
9857
+ }
9858
+ }
9859
+ const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
9860
+ const ariaHidden = clean.match(ariaHiddenPattern);
9861
+ if (ariaHidden) {
9862
+ stripped += ariaHidden.length;
9863
+ warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
9864
+ ariaHiddenPattern.lastIndex = 0;
9865
+ clean = clean.replace(ariaHiddenPattern, "");
9866
+ }
9867
+ const textResult = sanitizeText(clean);
9868
+ return {
9869
+ text: textResult.text,
9870
+ stripped: stripped + textResult.stripped,
9871
+ warnings: [...warnings, ...textResult.warnings]
9872
+ };
9873
+ }
9874
+ var INJECTION_PATTERNS;
9875
+ var init_sanitize = __esm(() => {
9876
+ INJECTION_PATTERNS = [
9877
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
9878
+ /ignore\s+(all\s+)?prior\s+instructions/gi,
9879
+ /disregard\s+(all\s+)?previous/gi,
9880
+ /forget\s+(all\s+)?previous/gi,
9881
+ /you\s+are\s+now\s+/gi,
9882
+ /new\s+instructions?\s*:/gi,
9883
+ /system\s+prompt\s*:/gi,
9884
+ /\[INST\]/gi,
9885
+ /\[\/INST\]/gi,
9886
+ /<\|im_start\|>/gi,
9887
+ /<\|im_end\|>/gi,
9888
+ /<<SYS>>/gi,
9889
+ /<<\/SYS>>/gi,
9890
+ /IMPORTANT:\s*ignore/gi,
9891
+ /CRITICAL:\s*override/gi,
9892
+ /assistant:\s/gi,
9893
+ /human:\s/gi
9894
+ ];
9895
+ });
9896
+
9204
9897
  // src/lib/annotate.ts
9205
9898
  var exports_annotate = {};
9206
9899
  __export(exports_annotate, {
@@ -9261,26 +9954,213 @@ var init_annotate = __esm(() => {
9261
9954
  import_sharp3 = __toESM(require_lib(), 1);
9262
9955
  });
9263
9956
 
9957
+ // src/lib/auth-flow.ts
9958
+ var exports_auth_flow = {};
9959
+ __export(exports_auth_flow, {
9960
+ tryReplayAuth: () => tryReplayAuth,
9961
+ touchAuthFlow: () => touchAuthFlow,
9962
+ saveAuthFlow: () => saveAuthFlow,
9963
+ listAuthFlows: () => listAuthFlows,
9964
+ isAuthRedirect: () => isAuthRedirect,
9965
+ isAuthPage: () => isAuthPage,
9966
+ getAuthFlowByName: () => getAuthFlowByName,
9967
+ getAuthFlowByDomain: () => getAuthFlowByDomain,
9968
+ getAuthFlow: () => getAuthFlow,
9969
+ deleteAuthFlow: () => deleteAuthFlow
9970
+ });
9971
+ import { randomUUID as randomUUID10 } from "crypto";
9972
+ function saveAuthFlow(data) {
9973
+ const db = getDatabase();
9974
+ const id = randomUUID10();
9975
+ db.prepare("INSERT OR REPLACE INTO auth_flows (id, name, domain, recording_id, storage_state_path) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.domain, data.recordingId ?? null, data.storageStatePath ?? null);
9976
+ return getAuthFlow(id);
9977
+ }
9978
+ function getAuthFlow(id) {
9979
+ const db = getDatabase();
9980
+ return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
9981
+ }
9982
+ function getAuthFlowByName(name) {
9983
+ const db = getDatabase();
9984
+ return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
9985
+ }
9986
+ function getAuthFlowByDomain(domain) {
9987
+ const db = getDatabase();
9988
+ return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
9989
+ }
9990
+ function listAuthFlows() {
9991
+ const db = getDatabase();
9992
+ return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
9993
+ }
9994
+ function deleteAuthFlow(name) {
9995
+ const db = getDatabase();
9996
+ const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
9997
+ return result.changes > 0;
9998
+ }
9999
+ function touchAuthFlow(id) {
10000
+ const db = getDatabase();
10001
+ db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
10002
+ }
10003
+ function isAuthPage(url) {
10004
+ return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
10005
+ }
10006
+ function isAuthRedirect(fromUrl, toUrl) {
10007
+ if (fromUrl === toUrl)
10008
+ return false;
10009
+ return isAuthPage(toUrl) && !isAuthPage(fromUrl);
10010
+ }
10011
+ async function tryReplayAuth(page, domain) {
10012
+ const flow = getAuthFlowByDomain(domain);
10013
+ if (!flow)
10014
+ return { replayed: false };
10015
+ if (flow.storage_state_path) {
10016
+ try {
10017
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
10018
+ if (existsSync5(flow.storage_state_path)) {
10019
+ const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
10020
+ if (state.cookies?.length) {
10021
+ await page.context().addCookies(state.cookies);
10022
+ await page.reload();
10023
+ await new Promise((r) => setTimeout(r, 1000));
10024
+ if (!isAuthPage(page.url())) {
10025
+ touchAuthFlow(flow.id);
10026
+ return { replayed: true, flow, method: "storage_state" };
10027
+ }
10028
+ }
10029
+ }
10030
+ } catch {}
10031
+ }
10032
+ if (flow.recording_id) {
10033
+ try {
10034
+ const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
10035
+ const result = await replayRecording2(flow.recording_id, page);
10036
+ if (result.success) {
10037
+ try {
10038
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
10039
+ const path = await saveStateFromPage2(page, flow.name);
10040
+ const db = getDatabase();
10041
+ db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
10042
+ } catch {}
10043
+ touchAuthFlow(flow.id);
10044
+ return { replayed: true, flow, method: "recording_replay" };
10045
+ }
10046
+ } catch {}
10047
+ }
10048
+ return { replayed: false, flow };
10049
+ }
10050
+ var AUTH_URL_PATTERNS;
10051
+ var init_auth_flow = __esm(() => {
10052
+ init_schema();
10053
+ AUTH_URL_PATTERNS = [
10054
+ /\/login/i,
10055
+ /\/signin/i,
10056
+ /\/sign-in/i,
10057
+ /\/auth/i,
10058
+ /\/sso/i,
10059
+ /\/oauth/i,
10060
+ /\/cas\/login/i,
10061
+ /accounts\.google\.com/i,
10062
+ /login\.microsoftonline\.com/i,
10063
+ /github\.com\/login/i,
10064
+ /auth0\.com/i
10065
+ ];
10066
+ });
10067
+
10068
+ // src/lib/vision-fallback.ts
10069
+ var exports_vision_fallback = {};
10070
+ __export(exports_vision_fallback, {
10071
+ findElementByVision: () => findElementByVision,
10072
+ clickByVision: () => clickByVision
10073
+ });
10074
+ async function findElementByVision(page, description, opts) {
10075
+ const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
10076
+ const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
10077
+ const base64 = screenshot.toString("base64");
10078
+ const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
10079
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
10080
+ if (!apiKey) {
10081
+ return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
10082
+ }
10083
+ try {
10084
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
10085
+ method: "POST",
10086
+ headers: {
10087
+ "content-type": "application/json",
10088
+ "x-api-key": apiKey,
10089
+ "anthropic-version": "2023-06-01"
10090
+ },
10091
+ body: JSON.stringify({
10092
+ model,
10093
+ max_tokens: 256,
10094
+ messages: [{
10095
+ role: "user",
10096
+ content: [
10097
+ {
10098
+ type: "image",
10099
+ source: { type: "base64", media_type: "image/jpeg", data: base64 }
10100
+ },
10101
+ {
10102
+ type: "text",
10103
+ text: `Find the element matching this description: "${description}"
10104
+
10105
+ The screenshot is ${viewport.width}x${viewport.height} pixels.
10106
+
10107
+ Reply with ONLY a JSON object (no markdown, no explanation):
10108
+ {"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
10109
+
10110
+ If you cannot find the element:
10111
+ {"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
10112
+ }
10113
+ ]
10114
+ }]
10115
+ })
10116
+ });
10117
+ const data = await response.json();
10118
+ const text = data.content?.[0]?.text ?? "";
10119
+ const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
10120
+ const result = JSON.parse(jsonStr);
10121
+ result.model = model;
10122
+ return result;
10123
+ } catch (err) {
10124
+ return {
10125
+ found: false,
10126
+ x: 0,
10127
+ y: 0,
10128
+ confidence: "none",
10129
+ description,
10130
+ model,
10131
+ error: err instanceof Error ? err.message : String(err)
10132
+ };
10133
+ }
10134
+ }
10135
+ async function clickByVision(page, description, opts) {
10136
+ const result = await findElementByVision(page, description, opts);
10137
+ if (result.found && result.x > 0 && result.y > 0) {
10138
+ await page.mouse.click(result.x, result.y);
10139
+ }
10140
+ return result;
10141
+ }
10142
+ var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
10143
+
9264
10144
  // src/lib/auth.ts
9265
10145
  var exports_auth = {};
9266
10146
  __export(exports_auth, {
9267
10147
  loginWithCredentials: () => loginWithCredentials,
9268
10148
  getCredentials: () => getCredentials
9269
10149
  });
9270
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
9271
- import { join as join8 } from "path";
9272
- import { homedir as homedir8 } from "os";
10150
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
10151
+ import { join as join9 } from "path";
10152
+ import { homedir as homedir9 } from "os";
9273
10153
  async function getCredentials(service) {
9274
10154
  try {
9275
- const { getSecret } = await import(`${homedir8()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
10155
+ const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
9276
10156
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
9277
10157
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
9278
10158
  if (email?.value && password?.value) {
9279
10159
  return { email: email.value, password: password.value };
9280
10160
  }
9281
10161
  } catch {}
9282
- const secretsPath = join8(homedir8(), ".secrets");
9283
- if (existsSync4(secretsPath)) {
10162
+ const secretsPath = join9(homedir9(), ".secrets");
10163
+ if (existsSync5(secretsPath)) {
9284
10164
  const content = readFileSync3(secretsPath, "utf8");
9285
10165
  const lines = content.split(`
9286
10166
  `);
@@ -9457,14 +10337,14 @@ __export(exports_dist, {
9457
10337
  InvalidScopeError: () => InvalidScopeError,
9458
10338
  EntityNotFoundError: () => EntityNotFoundError,
9459
10339
  DuplicateMemoryError: () => DuplicateMemoryError,
9460
- DEFAULT_MODEL: () => DEFAULT_MODEL,
10340
+ DEFAULT_MODEL: () => DEFAULT_MODEL2,
9461
10341
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
9462
10342
  });
9463
10343
  import { Database as Database2 } from "bun:sqlite";
9464
- import { existsSync as existsSync5, mkdirSync as mkdirSync8 } from "fs";
9465
- import { dirname, join as join9, resolve } from "path";
9466
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
9467
- import { homedir as homedir9 } from "os";
10344
+ import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
10345
+ import { dirname, join as join10, resolve } from "path";
10346
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
10347
+ import { homedir as homedir10 } from "os";
9468
10348
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
9469
10349
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
9470
10350
  import { homedir as homedir22 } from "os";
@@ -9478,8 +10358,8 @@ function isInMemoryDb(path) {
9478
10358
  function findNearestMementosDb(startDir) {
9479
10359
  let dir = resolve(startDir);
9480
10360
  while (true) {
9481
- const candidate = join9(dir, ".mementos", "mementos.db");
9482
- if (existsSync5(candidate))
10361
+ const candidate = join10(dir, ".mementos", "mementos.db");
10362
+ if (existsSync6(candidate))
9483
10363
  return candidate;
9484
10364
  const parent = dirname(dir);
9485
10365
  if (parent === dir)
@@ -9491,7 +10371,7 @@ function findNearestMementosDb(startDir) {
9491
10371
  function findGitRoot(startDir) {
9492
10372
  let dir = resolve(startDir);
9493
10373
  while (true) {
9494
- if (existsSync5(join9(dir, ".git")))
10374
+ if (existsSync6(join10(dir, ".git")))
9495
10375
  return dir;
9496
10376
  const parent = dirname(dir);
9497
10377
  if (parent === dir)
@@ -9511,25 +10391,25 @@ function getDbPath() {
9511
10391
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
9512
10392
  const gitRoot = findGitRoot(cwd);
9513
10393
  if (gitRoot) {
9514
- return join9(gitRoot, ".mementos", "mementos.db");
10394
+ return join10(gitRoot, ".mementos", "mementos.db");
9515
10395
  }
9516
10396
  }
9517
10397
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
9518
- return join9(home, ".mementos", "mementos.db");
10398
+ return join10(home, ".mementos", "mementos.db");
9519
10399
  }
9520
- function ensureDir(filePath) {
10400
+ function ensureDir2(filePath) {
9521
10401
  if (isInMemoryDb(filePath))
9522
10402
  return;
9523
10403
  const dir = dirname(resolve(filePath));
9524
- if (!existsSync5(dir)) {
9525
- mkdirSync8(dir, { recursive: true });
10404
+ if (!existsSync6(dir)) {
10405
+ mkdirSync9(dir, { recursive: true });
9526
10406
  }
9527
10407
  }
9528
10408
  function getDatabase2(dbPath) {
9529
10409
  if (_db2)
9530
10410
  return _db2;
9531
10411
  const path = dbPath || getDbPath();
9532
- ensureDir(path);
10412
+ ensureDir2(path);
9533
10413
  _db2 = new Database2(path, { create: true });
9534
10414
  _db2.run("PRAGMA journal_mode = WAL");
9535
10415
  _db2.run("PRAGMA busy_timeout = 5000");
@@ -11372,7 +12252,7 @@ function isValidCategory(value) {
11372
12252
  return VALID_CATEGORIES.includes(value);
11373
12253
  }
11374
12254
  function loadConfig() {
11375
- const configPath = join22(homedir9(), ".mementos", "config.json");
12255
+ const configPath = join22(homedir10(), ".mementos", "config.json");
11376
12256
  let fileConfig = {};
11377
12257
  if (existsSync22(configPath)) {
11378
12258
  try {
@@ -11399,10 +12279,10 @@ function loadConfig() {
11399
12279
  return merged;
11400
12280
  }
11401
12281
  function profilesDir() {
11402
- return join22(homedir9(), ".mementos", "profiles");
12282
+ return join22(homedir10(), ".mementos", "profiles");
11403
12283
  }
11404
12284
  function globalConfigPath() {
11405
- return join22(homedir9(), ".mementos", "config.json");
12285
+ return join22(homedir10(), ".mementos", "config.json");
11406
12286
  }
11407
12287
  function readGlobalConfig() {
11408
12288
  const p = globalConfigPath();
@@ -11416,7 +12296,7 @@ function readGlobalConfig() {
11416
12296
  }
11417
12297
  function writeGlobalConfig(data) {
11418
12298
  const p = globalConfigPath();
11419
- ensureDir2(dirname2(p));
12299
+ ensureDir22(dirname2(p));
11420
12300
  writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
11421
12301
  }
11422
12302
  function getActiveProfile() {
@@ -11439,18 +12319,18 @@ function listProfiles2() {
11439
12319
  const dir = profilesDir();
11440
12320
  if (!existsSync22(dir))
11441
12321
  return [];
11442
- return readdirSync3(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
12322
+ return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
11443
12323
  }
11444
12324
  function deleteProfile2(name) {
11445
12325
  const dbPath = join22(profilesDir(), `${name}.db`);
11446
12326
  if (!existsSync22(dbPath))
11447
12327
  return false;
11448
- unlinkSync2(dbPath);
12328
+ unlinkSync3(dbPath);
11449
12329
  if (getActiveProfile() === name)
11450
12330
  setActiveProfile(null);
11451
12331
  return true;
11452
12332
  }
11453
- function ensureDir2(dir) {
12333
+ function ensureDir22(dir) {
11454
12334
  if (!existsSync22(dir)) {
11455
12335
  mkdirSync22(dir, { recursive: true });
11456
12336
  }
@@ -12572,7 +13452,7 @@ function writeConfig(config) {
12572
13452
  }
12573
13453
  function getActiveModel() {
12574
13454
  const config = readConfig();
12575
- return config.activeModel ?? DEFAULT_MODEL;
13455
+ return config.activeModel ?? DEFAULT_MODEL2;
12576
13456
  }
12577
13457
  function setActiveModel(modelId) {
12578
13458
  const config = readConfig();
@@ -12646,7 +13526,7 @@ Return JSON with this exact shape:
12646
13526
  examples: finalExamples,
12647
13527
  count: finalExamples.length
12648
13528
  };
12649
- }, DEFAULT_MODEL = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
13529
+ }, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
12650
13530
  var init_dist = __esm(() => {
12651
13531
  __defProp2 = Object.defineProperty;
12652
13532
  exports_database = {};
@@ -14082,10 +14962,10 @@ __export(exports_dist2, {
14082
14962
  acquireLock: () => acquireLock2
14083
14963
  });
14084
14964
  import { Database as Database3 } from "bun:sqlite";
14085
- import { mkdirSync as mkdirSync9 } from "fs";
14086
- import { join as join10, dirname as dirname3 } from "path";
14087
- import { homedir as homedir10 } from "os";
14088
- import { randomUUID as randomUUID10 } from "crypto";
14965
+ import { mkdirSync as mkdirSync10 } from "fs";
14966
+ import { join as join11, dirname as dirname3 } from "path";
14967
+ import { homedir as homedir11 } from "os";
14968
+ import { randomUUID as randomUUID11 } from "crypto";
14089
14969
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
14090
14970
  import { join as join33 } from "path";
14091
14971
  import { homedir as homedir33 } from "os";
@@ -14099,13 +14979,13 @@ import { homedir as homedir42 } from "os";
14099
14979
  function getDbPath2() {
14100
14980
  if (process.env.CONVERSATIONS_DB_PATH)
14101
14981
  return process.env.CONVERSATIONS_DB_PATH;
14102
- return join10(homedir10(), ".conversations", "messages.db");
14982
+ return join11(homedir11(), ".conversations", "messages.db");
14103
14983
  }
14104
14984
  function getDb() {
14105
14985
  if (db)
14106
14986
  return db;
14107
14987
  const dbPath = getDbPath2();
14108
- mkdirSync9(dirname3(dbPath), { recursive: true });
14988
+ mkdirSync10(dirname3(dbPath), { recursive: true });
14109
14989
  db = new Database3(dbPath, { create: true });
14110
14990
  db.exec("PRAGMA journal_mode = WAL");
14111
14991
  db.exec("PRAGMA busy_timeout = 5000");
@@ -14465,7 +15345,7 @@ function guessMimeType(name) {
14465
15345
  function sendMessage(opts) {
14466
15346
  const db2 = getDb();
14467
15347
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
14468
- const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID10().slice(0, 8)}`);
15348
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
14469
15349
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
14470
15350
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
14471
15351
  const blocking = opts.blocking ? 1 : 0;
@@ -18507,11 +19387,11 @@ __export(exports_dist3, {
18507
19387
  AgentNotFoundError: () => AgentNotFoundError2
18508
19388
  });
18509
19389
  import { Database as Database4 } from "bun:sqlite";
18510
- import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
18511
- import { dirname as dirname5, join as join11, resolve as resolve3 } from "path";
19390
+ import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
19391
+ import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
18512
19392
  import { existsSync as existsSync33 } from "fs";
18513
19393
  import { join as join34 } from "path";
18514
- import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
19394
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
18515
19395
  import { join as join24 } from "path";
18516
19396
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
18517
19397
  import { join as join44 } from "path";
@@ -18741,8 +19621,8 @@ function isInMemoryDb2(path) {
18741
19621
  function findNearestTodosDb(startDir) {
18742
19622
  let dir = resolve3(startDir);
18743
19623
  while (true) {
18744
- const candidate = join11(dir, ".todos", "todos.db");
18745
- if (existsSync6(candidate))
19624
+ const candidate = join12(dir, ".todos", "todos.db");
19625
+ if (existsSync7(candidate))
18746
19626
  return candidate;
18747
19627
  const parent = dirname5(dir);
18748
19628
  if (parent === dir)
@@ -18754,7 +19634,7 @@ function findNearestTodosDb(startDir) {
18754
19634
  function findGitRoot2(startDir) {
18755
19635
  let dir = resolve3(startDir);
18756
19636
  while (true) {
18757
- if (existsSync6(join11(dir, ".git")))
19637
+ if (existsSync7(join12(dir, ".git")))
18758
19638
  return dir;
18759
19639
  const parent = dirname5(dir);
18760
19640
  if (parent === dir)
@@ -18774,18 +19654,18 @@ function getDbPath3() {
18774
19654
  if (process.env["TODOS_DB_SCOPE"] === "project") {
18775
19655
  const gitRoot = findGitRoot2(cwd);
18776
19656
  if (gitRoot) {
18777
- return join11(gitRoot, ".todos", "todos.db");
19657
+ return join12(gitRoot, ".todos", "todos.db");
18778
19658
  }
18779
19659
  }
18780
19660
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
18781
- return join11(home, ".todos", "todos.db");
19661
+ return join12(home, ".todos", "todos.db");
18782
19662
  }
18783
19663
  function ensureDir3(filePath) {
18784
19664
  if (isInMemoryDb2(filePath))
18785
19665
  return;
18786
19666
  const dir = dirname5(resolve3(filePath));
18787
- if (!existsSync6(dir)) {
18788
- mkdirSync10(dir, { recursive: true });
19667
+ if (!existsSync7(dir)) {
19668
+ mkdirSync11(dir, { recursive: true });
18789
19669
  }
18790
19670
  }
18791
19671
  function getDatabase3(dbPath) {
@@ -19244,14 +20124,14 @@ function ensureProject2(name, path, db2) {
19244
20124
  }
19245
20125
  return createProject3({ name, path }, d);
19246
20126
  }
19247
- function ensureDir22(dir) {
20127
+ function ensureDir23(dir) {
19248
20128
  if (!existsSync23(dir))
19249
20129
  mkdirSync24(dir, { recursive: true });
19250
20130
  }
19251
20131
  function listJsonFiles(dir) {
19252
20132
  if (!existsSync23(dir))
19253
20133
  return [];
19254
- return readdirSync4(dir).filter((f) => f.endsWith(".json"));
20134
+ return readdirSync5(dir).filter((f) => f.endsWith(".json"));
19255
20135
  }
19256
20136
  function readJsonFile(path) {
19257
20137
  try {
@@ -22116,7 +22996,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
22116
22996
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
22117
22997
  const dir = getTaskListDir(taskListId);
22118
22998
  if (!existsSync43(dir))
22119
- ensureDir22(dir);
22999
+ ensureDir23(dir);
22120
23000
  const filter = {};
22121
23001
  if (projectId)
22122
23002
  filter["project_id"] = projectId;
@@ -22334,7 +23214,7 @@ function metadataKey(agent) {
22334
23214
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
22335
23215
  const dir = getTaskListDir2(agent, taskListId);
22336
23216
  if (!existsSync52(dir))
22337
- ensureDir22(dir);
23217
+ ensureDir23(dir);
22338
23218
  const filter = {};
22339
23219
  if (projectId)
22340
23220
  filter["project_id"] = projectId;
@@ -23862,9 +24742,9 @@ __export(exports_dist4, {
23862
24742
  CATEGORIES: () => CATEGORIES,
23863
24743
  AGENT_TARGETS: () => AGENT_TARGETS
23864
24744
  });
23865
- import { existsSync as existsSync7, cpSync, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync5, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
23866
- import { join as join12, dirname as dirname6 } from "path";
23867
- import { homedir as homedir11 } from "os";
24745
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
24746
+ import { join as join13, dirname as dirname6 } from "path";
24747
+ import { homedir as homedir12 } from "os";
23868
24748
  import { fileURLToPath } from "url";
23869
24749
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
23870
24750
  import { join as join25 } from "path";
@@ -23975,35 +24855,35 @@ function normalizeSkillName(name) {
23975
24855
  function findSkillsDir() {
23976
24856
  let dir = __dirname2;
23977
24857
  for (let i = 0;i < 5; i++) {
23978
- const candidate = join12(dir, "skills");
23979
- if (existsSync7(candidate)) {
24858
+ const candidate = join13(dir, "skills");
24859
+ if (existsSync8(candidate)) {
23980
24860
  return candidate;
23981
24861
  }
23982
24862
  dir = dirname6(dir);
23983
24863
  }
23984
- return join12(__dirname2, "..", "skills");
24864
+ return join13(__dirname2, "..", "skills");
23985
24865
  }
23986
24866
  function getSkillPath(name) {
23987
24867
  const skillName = normalizeSkillName(name);
23988
- return join12(SKILLS_DIR, skillName);
24868
+ return join13(SKILLS_DIR, skillName);
23989
24869
  }
23990
24870
  function skillExists(name) {
23991
- return existsSync7(getSkillPath(name));
24871
+ return existsSync8(getSkillPath(name));
23992
24872
  }
23993
24873
  function installSkill(name, options = {}) {
23994
24874
  const { targetDir = process.cwd(), overwrite = false } = options;
23995
24875
  const skillName = normalizeSkillName(name);
23996
24876
  const sourcePath = getSkillPath(name);
23997
- const destDir = join12(targetDir, ".skills");
23998
- const destPath = join12(destDir, skillName);
23999
- if (!existsSync7(sourcePath)) {
24877
+ const destDir = join13(targetDir, ".skills");
24878
+ const destPath = join13(destDir, skillName);
24879
+ if (!existsSync8(sourcePath)) {
24000
24880
  return {
24001
24881
  skill: name,
24002
24882
  success: false,
24003
24883
  error: `Skill '${name}' not found`
24004
24884
  };
24005
24885
  }
24006
- if (existsSync7(destPath) && !overwrite) {
24886
+ if (existsSync8(destPath) && !overwrite) {
24007
24887
  return {
24008
24888
  skill: name,
24009
24889
  success: false,
@@ -24012,10 +24892,10 @@ function installSkill(name, options = {}) {
24012
24892
  };
24013
24893
  }
24014
24894
  try {
24015
- if (!existsSync7(destDir)) {
24016
- mkdirSync11(destDir, { recursive: true });
24895
+ if (!existsSync8(destDir)) {
24896
+ mkdirSync12(destDir, { recursive: true });
24017
24897
  }
24018
- if (existsSync7(destPath) && overwrite) {
24898
+ if (existsSync8(destPath) && overwrite) {
24019
24899
  rmSync2(destPath, { recursive: true, force: true });
24020
24900
  }
24021
24901
  cpSync(sourcePath, destPath, {
@@ -24054,10 +24934,10 @@ function installSkills(names, options = {}) {
24054
24934
  return names.map((name) => installSkill(name, options));
24055
24935
  }
24056
24936
  function updateSkillsIndex(skillsDir) {
24057
- const indexPath = join12(skillsDir, "index.ts");
24937
+ const indexPath = join13(skillsDir, "index.ts");
24058
24938
  const meta = loadMeta(skillsDir);
24059
24939
  const disabledSet = new Set(meta.disabled || []);
24060
- const skills = readdirSync5(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
24940
+ const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
24061
24941
  const exports = skills.map((s) => {
24062
24942
  const name = s.replace("skill-", "").replace(/-/g, "_");
24063
24943
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -24073,11 +24953,11 @@ ${exports}
24073
24953
  writeFileSync6(indexPath, content);
24074
24954
  }
24075
24955
  function getMetaPath(skillsDir) {
24076
- return join12(skillsDir, ".meta.json");
24956
+ return join13(skillsDir, ".meta.json");
24077
24957
  }
24078
24958
  function loadMeta(skillsDir) {
24079
24959
  const metaPath2 = getMetaPath(skillsDir);
24080
- if (existsSync7(metaPath2)) {
24960
+ if (existsSync8(metaPath2)) {
24081
24961
  try {
24082
24962
  return JSON.parse(readFileSync7(metaPath2, "utf-8"));
24083
24963
  } catch {}
@@ -24092,8 +24972,8 @@ function recordInstall(skillsDir, name) {
24092
24972
  const skillName = normalizeSkillName(name);
24093
24973
  let version = "unknown";
24094
24974
  try {
24095
- const pkgPath = join12(skillsDir, skillName, "package.json");
24096
- if (existsSync7(pkgPath)) {
24975
+ const pkgPath = join13(skillsDir, skillName, "package.json");
24976
+ if (existsSync8(pkgPath)) {
24097
24977
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
24098
24978
  version = pkg.version || "unknown";
24099
24979
  }
@@ -24107,12 +24987,12 @@ function recordRemove(skillsDir, name) {
24107
24987
  saveMeta(skillsDir, meta);
24108
24988
  }
24109
24989
  function getInstallMeta(targetDir = process.cwd()) {
24110
- return loadMeta(join12(targetDir, ".skills"));
24990
+ return loadMeta(join13(targetDir, ".skills"));
24111
24991
  }
24112
24992
  function disableSkill(name, targetDir = process.cwd()) {
24113
- const skillsDir = join12(targetDir, ".skills");
24993
+ const skillsDir = join13(targetDir, ".skills");
24114
24994
  const skillName = normalizeSkillName(name);
24115
- if (!existsSync7(join12(skillsDir, skillName)))
24995
+ if (!existsSync8(join13(skillsDir, skillName)))
24116
24996
  return false;
24117
24997
  const meta = loadMeta(skillsDir);
24118
24998
  const disabled = new Set(meta.disabled || []);
@@ -24125,7 +25005,7 @@ function disableSkill(name, targetDir = process.cwd()) {
24125
25005
  return true;
24126
25006
  }
24127
25007
  function enableSkill(name, targetDir = process.cwd()) {
24128
- const skillsDir = join12(targetDir, ".skills");
25008
+ const skillsDir = join13(targetDir, ".skills");
24129
25009
  const meta = loadMeta(skillsDir);
24130
25010
  const disabled = new Set(meta.disabled || []);
24131
25011
  if (!disabled.has(name))
@@ -24137,24 +25017,24 @@ function enableSkill(name, targetDir = process.cwd()) {
24137
25017
  return true;
24138
25018
  }
24139
25019
  function getDisabledSkills(targetDir = process.cwd()) {
24140
- const meta = loadMeta(join12(targetDir, ".skills"));
25020
+ const meta = loadMeta(join13(targetDir, ".skills"));
24141
25021
  return meta.disabled || [];
24142
25022
  }
24143
25023
  function getInstalledSkills(targetDir = process.cwd()) {
24144
- const skillsDir = join12(targetDir, ".skills");
24145
- if (!existsSync7(skillsDir)) {
25024
+ const skillsDir = join13(targetDir, ".skills");
25025
+ if (!existsSync8(skillsDir)) {
24146
25026
  return [];
24147
25027
  }
24148
- return readdirSync5(skillsDir).filter((f) => {
24149
- const fullPath = join12(skillsDir, f);
25028
+ return readdirSync6(skillsDir).filter((f) => {
25029
+ const fullPath = join13(skillsDir, f);
24150
25030
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
24151
25031
  }).map((f) => f.replace("skill-", ""));
24152
25032
  }
24153
25033
  function removeSkill(name, targetDir = process.cwd()) {
24154
25034
  const skillName = normalizeSkillName(name);
24155
- const skillsDir = join12(targetDir, ".skills");
24156
- const skillPath = join12(skillsDir, skillName);
24157
- if (!existsSync7(skillPath)) {
25035
+ const skillsDir = join13(targetDir, ".skills");
25036
+ const skillPath = join13(skillsDir, skillName);
25037
+ if (!existsSync8(skillPath)) {
24158
25038
  return false;
24159
25039
  }
24160
25040
  rmSync2(skillPath, { recursive: true, force: true });
@@ -24165,24 +25045,24 @@ function removeSkill(name, targetDir = process.cwd()) {
24165
25045
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
24166
25046
  const agentDir = `.${agent}`;
24167
25047
  if (scope === "project") {
24168
- return join12(projectDir || process.cwd(), agentDir, "skills");
25048
+ return join13(projectDir || process.cwd(), agentDir, "skills");
24169
25049
  }
24170
- return join12(homedir11(), agentDir, "skills");
25050
+ return join13(homedir12(), agentDir, "skills");
24171
25051
  }
24172
25052
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
24173
25053
  const skillName = normalizeSkillName(name);
24174
- return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
25054
+ return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
24175
25055
  }
24176
25056
  function installSkillForAgent(name, options, generateSkillMd) {
24177
25057
  const { agent, scope = "global", projectDir } = options;
24178
25058
  const skillName = normalizeSkillName(name);
24179
25059
  const sourcePath = getSkillPath(name);
24180
- if (!existsSync7(sourcePath)) {
25060
+ if (!existsSync8(sourcePath)) {
24181
25061
  return { skill: name, success: false, error: `Skill '${name}' not found` };
24182
25062
  }
24183
25063
  let skillMdContent = null;
24184
- const skillMdPath = join12(sourcePath, "SKILL.md");
24185
- if (existsSync7(skillMdPath)) {
25064
+ const skillMdPath = join13(sourcePath, "SKILL.md");
25065
+ if (existsSync8(skillMdPath)) {
24186
25066
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
24187
25067
  } else if (generateSkillMd) {
24188
25068
  skillMdContent = generateSkillMd(name);
@@ -24192,8 +25072,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
24192
25072
  }
24193
25073
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
24194
25074
  if (scope === "global") {
24195
- const agentBaseDir2 = join12(homedir11(), `.${agent}`);
24196
- if (!existsSync7(agentBaseDir2)) {
25075
+ const agentBaseDir2 = join13(homedir12(), `.${agent}`);
25076
+ if (!existsSync8(agentBaseDir2)) {
24197
25077
  const agentLabels = {
24198
25078
  claude: "Claude Code",
24199
25079
  codex: "Codex CLI",
@@ -24216,8 +25096,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
24216
25096
  }
24217
25097
  }
24218
25098
  try {
24219
- mkdirSync11(destDir, { recursive: true });
24220
- writeFileSync6(join12(destDir, "SKILL.md"), skillMdContent);
25099
+ mkdirSync12(destDir, { recursive: true });
25100
+ writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
24221
25101
  return { skill: name, success: true, path: destDir };
24222
25102
  } catch (error) {
24223
25103
  return {
@@ -24230,7 +25110,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
24230
25110
  function removeSkillForAgent(name, options) {
24231
25111
  const { agent, scope = "global", projectDir } = options;
24232
25112
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
24233
- if (!existsSync7(destDir)) {
25113
+ if (!existsSync8(destDir)) {
24234
25114
  return false;
24235
25115
  }
24236
25116
  rmSync2(destDir, { recursive: true, force: true });
@@ -26154,7 +27034,7 @@ __export(exports_cron_manager, {
26154
27034
  deleteCronJob: () => deleteCronJob,
26155
27035
  createCronJob: () => createCronJob
26156
27036
  });
26157
- import { randomUUID as randomUUID11 } from "crypto";
27037
+ import { randomUUID as randomUUID12 } from "crypto";
26158
27038
  function ensureCronTable() {
26159
27039
  const db2 = getDatabase();
26160
27040
  db2.exec(`
@@ -26183,7 +27063,7 @@ function ensureCronTable() {
26183
27063
  function createCronJob(schedule, task, name) {
26184
27064
  ensureCronTable();
26185
27065
  const db2 = getDatabase();
26186
- const id = randomUUID11();
27066
+ const id = randomUUID12();
26187
27067
  db2.prepare(`
26188
27068
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
26189
27069
  VALUES (?, ?, ?, ?, 1)
@@ -26241,7 +27121,7 @@ function getCronEvents(jobId, limit = 10) {
26241
27121
  async function executeCronJob(job) {
26242
27122
  ensureCronTable();
26243
27123
  const db2 = getDatabase();
26244
- const eventId = randomUUID11();
27124
+ const eventId = randomUUID12();
26245
27125
  const startedAt = new Date().toISOString();
26246
27126
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
26247
27127
  try {
@@ -26332,7 +27212,7 @@ __export(exports_url_watcher, {
26332
27212
  deleteWatchJob: () => deleteWatchJob,
26333
27213
  createWatchJob: () => createWatchJob
26334
27214
  });
26335
- import { randomUUID as randomUUID12 } from "crypto";
27215
+ import { randomUUID as randomUUID13 } from "crypto";
26336
27216
  import { createHash } from "crypto";
26337
27217
  function ensureWatchTables() {
26338
27218
  const db2 = getDatabase();
@@ -26364,7 +27244,7 @@ function ensureWatchTables() {
26364
27244
  function createWatchJob(url, schedule, opts) {
26365
27245
  ensureWatchTables();
26366
27246
  const db2 = getDatabase();
26367
- const id = randomUUID12();
27247
+ const id = randomUUID13();
26368
27248
  db2.prepare(`
26369
27249
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
26370
27250
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -26400,7 +27280,7 @@ function getWatchEvents(watchId, limit = 20) {
26400
27280
  async function checkWatchJob(job) {
26401
27281
  ensureWatchTables();
26402
27282
  const db2 = getDatabase();
26403
- const eventId = randomUUID12();
27283
+ const eventId = randomUUID13();
26404
27284
  const checkedAt = new Date().toISOString();
26405
27285
  let newContent = "";
26406
27286
  try {
@@ -30552,23 +31432,23 @@ var NEVER = INVALID;
30552
31432
  init_session();
30553
31433
  init_actions();
30554
31434
  import { readFileSync as readFileSync8 } from "fs";
30555
- import { join as join13 } from "path";
31435
+ import { join as join14 } from "path";
30556
31436
 
30557
31437
  // src/lib/screenshot.ts
30558
31438
  init_types();
30559
31439
  init_gallery();
30560
31440
  var import_sharp = __toESM(require_lib(), 1);
30561
- import { join as join3 } from "path";
30562
- import { mkdirSync as mkdirSync3 } from "fs";
30563
- import { homedir as homedir3 } from "os";
31441
+ import { join as join4 } from "path";
31442
+ import { mkdirSync as mkdirSync4 } from "fs";
31443
+ import { homedir as homedir4 } from "os";
30564
31444
  function getDataDir2() {
30565
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
31445
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
30566
31446
  }
30567
31447
  function getScreenshotDir(projectId) {
30568
- const base = join3(getDataDir2(), "screenshots");
31448
+ const base = join4(getDataDir2(), "screenshots");
30569
31449
  const date = new Date().toISOString().split("T")[0];
30570
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
30571
- mkdirSync3(dir, { recursive: true });
31450
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
31451
+ mkdirSync4(dir, { recursive: true });
30572
31452
  return dir;
30573
31453
  }
30574
31454
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -30583,7 +31463,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
30583
31463
  }
30584
31464
  }
30585
31465
  async function generateThumbnail(raw, dir, stem) {
30586
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
31466
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
30587
31467
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
30588
31468
  await Bun.write(thumbPath, thumbBuffer);
30589
31469
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -30640,7 +31520,7 @@ async function takeScreenshot(page, opts) {
30640
31520
  const compressedSizeBytes = finalBuffer.length;
30641
31521
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
30642
31522
  const ext = format;
30643
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
31523
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
30644
31524
  await Bun.write(screenshotPath, finalBuffer);
30645
31525
  let thumbnailPath;
30646
31526
  let thumbnailBase64;
@@ -30700,12 +31580,12 @@ async function takeScreenshot(page, opts) {
30700
31580
  }
30701
31581
  async function generatePDF(page, opts) {
30702
31582
  try {
30703
- const base = join3(getDataDir2(), "pdfs");
31583
+ const base = join4(getDataDir2(), "pdfs");
30704
31584
  const date = new Date().toISOString().split("T")[0];
30705
- const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
30706
- mkdirSync3(dir, { recursive: true });
31585
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
31586
+ mkdirSync4(dir, { recursive: true });
30707
31587
  const timestamp = Date.now();
30708
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
31588
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
30709
31589
  const buffer = await page.pdf({
30710
31590
  path: pdfPath,
30711
31591
  format: opts?.format ?? "A4",
@@ -30726,118 +31606,8 @@ async function generatePDF(page, opts) {
30726
31606
  // src/mcp/index.ts
30727
31607
  init_network();
30728
31608
 
30729
- // src/engines/cdp.ts
30730
- init_types();
30731
-
30732
- class CDPClient {
30733
- session;
30734
- networkEnabled = false;
30735
- performanceEnabled = false;
30736
- constructor(session) {
30737
- this.session = session;
30738
- }
30739
- static async fromPage(page) {
30740
- try {
30741
- const session = await page.context().newCDPSession(page);
30742
- return new CDPClient(session);
30743
- } catch (err) {
30744
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
30745
- }
30746
- }
30747
- async send(method, params) {
30748
- try {
30749
- return await this.session.send(method, params);
30750
- } catch (err) {
30751
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
30752
- }
30753
- }
30754
- on(event, handler) {
30755
- this.session.on(event, handler);
30756
- }
30757
- off(event, handler) {
30758
- this.session.off(event, handler);
30759
- }
30760
- async enableNetwork() {
30761
- if (!this.networkEnabled) {
30762
- await this.send("Network.enable");
30763
- this.networkEnabled = true;
30764
- }
30765
- }
30766
- async enablePerformance() {
30767
- if (!this.performanceEnabled) {
30768
- await this.send("Performance.enable");
30769
- this.performanceEnabled = true;
30770
- }
30771
- }
30772
- async getPerformanceMetrics() {
30773
- await this.enablePerformance();
30774
- const result = await this.send("Performance.getMetrics");
30775
- const m = {};
30776
- for (const metric of result.metrics) {
30777
- m[metric.name] = metric.value;
30778
- }
30779
- return {
30780
- js_heap_size_used: m["JSHeapUsedSize"],
30781
- js_heap_size_total: m["JSHeapTotalSize"],
30782
- dom_interactive: m["DOMInteractive"],
30783
- dom_complete: m["DOMComplete"],
30784
- load_event: m["LoadEventEnd"]
30785
- };
30786
- }
30787
- async startJSCoverage() {
30788
- await this.send("Profiler.enable");
30789
- await this.send("Debugger.enable");
30790
- await this.send("Profiler.startPreciseCoverage", {
30791
- callCount: false,
30792
- detailed: true
30793
- });
30794
- }
30795
- async stopJSCoverage() {
30796
- const result = await this.send("Profiler.takePreciseCoverage");
30797
- await this.send("Profiler.stopPreciseCoverage");
30798
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
30799
- url: r.url,
30800
- text: "",
30801
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
30802
- }));
30803
- }
30804
- async getCoverage() {
30805
- await this.startJSCoverage();
30806
- const js = await this.stopJSCoverage();
30807
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
30808
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
30809
- }
30810
- async captureHAREntries(page, handler) {
30811
- await this.enableNetwork();
30812
- const requestTimings = new Map;
30813
- const onRequest = (params) => {
30814
- requestTimings.set(params.requestId, params.timestamp);
30815
- };
30816
- const onResponse = (params) => {
30817
- const start = requestTimings.get(params.requestId);
30818
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
30819
- handler({
30820
- method: "GET",
30821
- url: params.response.url,
30822
- status: params.response.status,
30823
- duration
30824
- });
30825
- };
30826
- this.on("Network.requestWillBeSent", onRequest);
30827
- this.on("Network.responseReceived", onResponse);
30828
- return () => {
30829
- this.off("Network.requestWillBeSent", onRequest);
30830
- this.off("Network.responseReceived", onResponse);
30831
- };
30832
- }
30833
- async detach() {
30834
- try {
30835
- await this.session.detach();
30836
- } catch {}
30837
- }
30838
- }
30839
-
30840
31609
  // src/lib/performance.ts
31610
+ init_cdp();
30841
31611
  async function getPerformanceMetrics(page) {
30842
31612
  const navTiming = await page.evaluate(() => {
30843
31613
  const t = performance.timing;
@@ -30929,144 +31699,8 @@ async function setSessionStorage(page, key, value) {
30929
31699
  await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
30930
31700
  }
30931
31701
 
30932
- // src/db/recordings.ts
30933
- init_schema();
30934
- init_types();
30935
- import { randomUUID as randomUUID5 } from "crypto";
30936
- function deserialize2(row) {
30937
- return {
30938
- ...row,
30939
- project_id: row.project_id ?? undefined,
30940
- start_url: row.start_url ?? undefined,
30941
- steps: JSON.parse(row.steps)
30942
- };
30943
- }
30944
- function createRecording(data) {
30945
- const db = getDatabase();
30946
- const id = randomUUID5();
30947
- db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
30948
- return getRecording(id);
30949
- }
30950
- function getRecording(id) {
30951
- const db = getDatabase();
30952
- const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
30953
- if (!row)
30954
- throw new RecordingNotFoundError(id);
30955
- return deserialize2(row);
30956
- }
30957
- function listRecordings(projectId) {
30958
- const db = getDatabase();
30959
- const rows = projectId ? db.query("SELECT * FROM recordings WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM recordings ORDER BY created_at DESC").all();
30960
- return rows.map(deserialize2);
30961
- }
30962
- function updateRecording(id, data) {
30963
- const db = getDatabase();
30964
- const fields = [];
30965
- const values = [];
30966
- if (data.name !== undefined) {
30967
- fields.push("name = ?");
30968
- values.push(data.name);
30969
- }
30970
- if (data.steps !== undefined) {
30971
- fields.push("steps = ?");
30972
- values.push(JSON.stringify(data.steps));
30973
- }
30974
- if (data.start_url !== undefined) {
30975
- fields.push("start_url = ?");
30976
- values.push(data.start_url ?? null);
30977
- }
30978
- if (fields.length === 0)
30979
- return getRecording(id);
30980
- values.push(id);
30981
- db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
30982
- return getRecording(id);
30983
- }
30984
-
30985
- // src/lib/recorder.ts
30986
- init_actions();
30987
- init_types();
30988
- var activeRecordings = new Map;
30989
- function startRecording(sessionId, name, startUrl) {
30990
- const steps = [];
30991
- const recording = createRecording({ name, start_url: startUrl, steps });
30992
- activeRecordings.set(recording.id, {
30993
- id: recording.id,
30994
- steps,
30995
- cleanup: () => {}
30996
- });
30997
- return recording;
30998
- }
30999
- function recordStep(recordingId, step) {
31000
- const active = activeRecordings.get(recordingId);
31001
- if (!active)
31002
- throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
31003
- active.steps.push({ ...step, timestamp: Date.now() });
31004
- }
31005
- function stopRecording(recordingId) {
31006
- const active = activeRecordings.get(recordingId);
31007
- if (!active)
31008
- throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
31009
- active.cleanup();
31010
- activeRecordings.delete(recordingId);
31011
- return updateRecording(recordingId, { steps: active.steps });
31012
- }
31013
- async function replayRecording(recordingId, page) {
31014
- const recording = getRecording(recordingId);
31015
- const startTime = Date.now();
31016
- let executed = 0;
31017
- let failed = 0;
31018
- const errors2 = [];
31019
- for (const step of recording.steps) {
31020
- try {
31021
- switch (step.type) {
31022
- case "navigate":
31023
- if (step.url)
31024
- await navigate(page, step.url);
31025
- break;
31026
- case "click":
31027
- if (step.selector)
31028
- await click(page, step.selector);
31029
- break;
31030
- case "type":
31031
- if (step.selector && step.value)
31032
- await type(page, step.selector, step.value);
31033
- break;
31034
- case "scroll":
31035
- await scroll(page, "down");
31036
- break;
31037
- case "hover":
31038
- if (step.selector) {
31039
- const el = await page.$(step.selector);
31040
- if (el)
31041
- await el.hover();
31042
- }
31043
- break;
31044
- case "evaluate":
31045
- if (step.value)
31046
- await page.evaluate(step.value);
31047
- break;
31048
- case "wait":
31049
- if (step.selector) {
31050
- await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
31051
- }
31052
- break;
31053
- }
31054
- executed++;
31055
- } catch (err) {
31056
- failed++;
31057
- errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
31058
- }
31059
- await new Promise((r) => setTimeout(r, 100));
31060
- }
31061
- return {
31062
- recording_id: recordingId,
31063
- success: failed === 0,
31064
- steps_executed: executed,
31065
- steps_failed: failed,
31066
- errors: errors2,
31067
- duration_ms: Date.now() - startTime
31068
- };
31069
- }
31702
+ // src/mcp/index.ts
31703
+ init_recorder();
31070
31704
 
31071
31705
  // src/lib/crawler.ts
31072
31706
  init_types();
@@ -31249,16 +31883,16 @@ init_gallery();
31249
31883
 
31250
31884
  // src/lib/downloads.ts
31251
31885
  import { randomUUID as randomUUID9 } from "crypto";
31252
- import { join as join4, basename, extname } from "path";
31253
- import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
31254
- import { homedir as homedir4 } from "os";
31886
+ import { join as join5, basename, extname } from "path";
31887
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
31888
+ import { homedir as homedir5 } from "os";
31255
31889
  function getDataDir3() {
31256
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
31890
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
31257
31891
  }
31258
31892
  function getDownloadsDir(sessionId) {
31259
- const base = join4(getDataDir3(), "downloads");
31260
- const dir = sessionId ? join4(base, sessionId) : base;
31261
- mkdirSync4(dir, { recursive: true });
31893
+ const base = join5(getDataDir3(), "downloads");
31894
+ const dir = sessionId ? join5(base, sessionId) : base;
31895
+ mkdirSync5(dir, { recursive: true });
31262
31896
  return dir;
31263
31897
  }
31264
31898
  function metaPath(filePath) {
@@ -31270,7 +31904,7 @@ function saveToDownloads(buffer, filename, opts) {
31270
31904
  const ext = extname(filename) || "";
31271
31905
  const stem = basename(filename, ext);
31272
31906
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
31273
- const filePath = join4(dir, uniqueName);
31907
+ const filePath = join5(dir, uniqueName);
31274
31908
  writeFileSync(filePath, buffer);
31275
31909
  const meta = {
31276
31910
  id,
@@ -31299,20 +31933,20 @@ function listDownloads(sessionId) {
31299
31933
  const dir = getDownloadsDir(sessionId);
31300
31934
  const results = [];
31301
31935
  function scanDir(d) {
31302
- if (!existsSync(d))
31936
+ if (!existsSync2(d))
31303
31937
  return;
31304
- const entries = readdirSync(d);
31938
+ const entries = readdirSync2(d);
31305
31939
  for (const entry of entries) {
31306
31940
  if (entry.endsWith(".meta.json"))
31307
31941
  continue;
31308
- const full = join4(d, entry);
31942
+ const full = join5(d, entry);
31309
31943
  const stat = statSync(full);
31310
31944
  if (stat.isDirectory()) {
31311
31945
  scanDir(full);
31312
31946
  continue;
31313
31947
  }
31314
31948
  const mpath = metaPath(full);
31315
- if (!existsSync(mpath))
31949
+ if (!existsSync2(mpath))
31316
31950
  continue;
31317
31951
  try {
31318
31952
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -31342,9 +31976,9 @@ function deleteDownload(id, sessionId) {
31342
31976
  if (!file)
31343
31977
  return false;
31344
31978
  try {
31345
- unlinkSync(file.path);
31346
- if (existsSync(file.meta_path))
31347
- unlinkSync(file.meta_path);
31979
+ unlinkSync2(file.path);
31980
+ if (existsSync2(file.meta_path))
31981
+ unlinkSync2(file.meta_path);
31348
31982
  return true;
31349
31983
  } catch {
31350
31984
  return false;
@@ -31390,9 +32024,9 @@ function detectType(filename) {
31390
32024
 
31391
32025
  // src/lib/gallery-diff.ts
31392
32026
  var import_sharp2 = __toESM(require_lib(), 1);
31393
- import { join as join5 } from "path";
31394
- import { mkdirSync as mkdirSync5 } from "fs";
31395
- import { homedir as homedir5 } from "os";
32027
+ import { join as join6 } from "path";
32028
+ import { mkdirSync as mkdirSync6 } from "fs";
32029
+ import { homedir as homedir6 } from "os";
31396
32030
  async function diffImages(path1, path2) {
31397
32031
  const img1 = import_sharp2.default(path1);
31398
32032
  const img2 = import_sharp2.default(path2);
@@ -31423,10 +32057,10 @@ async function diffImages(path1, path2) {
31423
32057
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
31424
32058
  }
31425
32059
  }
31426
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
31427
- const diffDir = join5(dataDir, "diffs");
31428
- mkdirSync5(diffDir, { recursive: true });
31429
- const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
32060
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
32061
+ const diffDir = join6(dataDir, "diffs");
32062
+ mkdirSync6(diffDir, { recursive: true });
32063
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
31430
32064
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
31431
32065
  await Bun.write(diffPath, diffImageBuffer);
31432
32066
  return {
@@ -31442,9 +32076,9 @@ async function diffImages(path1, path2) {
31442
32076
  init_snapshot();
31443
32077
 
31444
32078
  // src/lib/files-integration.ts
31445
- import { join as join6 } from "path";
31446
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
31447
- import { homedir as homedir6 } from "os";
32079
+ import { join as join7 } from "path";
32080
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
32081
+ import { homedir as homedir7 } from "os";
31448
32082
  async function persistFile(localPath, opts) {
31449
32083
  try {
31450
32084
  const mod = await import("@hasna/files");
@@ -31453,12 +32087,12 @@ async function persistFile(localPath, opts) {
31453
32087
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
31454
32088
  }
31455
32089
  } catch {}
31456
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
32090
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
31457
32091
  const date = new Date().toISOString().split("T")[0];
31458
- const dir = join6(dataDir, "persistent", date);
31459
- mkdirSync6(dir, { recursive: true });
32092
+ const dir = join7(dataDir, "persistent", date);
32093
+ mkdirSync7(dir, { recursive: true });
31460
32094
  const filename = localPath.split("/").pop() ?? "file";
31461
- const targetPath = join6(dir, filename);
32095
+ const targetPath = join7(dir, filename);
31462
32096
  copyFileSync2(localPath, targetPath);
31463
32097
  return {
31464
32098
  id: `local-${Date.now()}`,
@@ -31468,6 +32102,9 @@ async function persistFile(localPath, opts) {
31468
32102
  };
31469
32103
  }
31470
32104
 
32105
+ // src/mcp/index.ts
32106
+ init_recordings();
32107
+
31471
32108
  // src/db/timeline.ts
31472
32109
  init_schema();
31473
32110
  function logEvent(sessionId, eventType, details = {}) {
@@ -31565,7 +32202,7 @@ async function closeTab(page, index) {
31565
32202
  init_dialogs();
31566
32203
  init_profiles();
31567
32204
  init_types();
31568
- var _pkg = JSON.parse(readFileSync8(join13(import.meta.dir, "../../package.json"), "utf8"));
32205
+ var _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
31569
32206
  var networkLogCleanup = new Map;
31570
32207
  var consoleCaptureCleanup = new Map;
31571
32208
  var harCaptures = new Map;
@@ -31612,7 +32249,7 @@ var server = new McpServer({
31612
32249
  name: "@hasna/browser",
31613
32250
  version: "0.0.1"
31614
32251
  });
31615
- server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected.", {
32252
+ server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected. Use cdp_url to attach to an already-running Chrome instance.", {
31616
32253
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
31617
32254
  use_case: exports_external.string().optional(),
31618
32255
  project_id: exports_external.string().optional(),
@@ -31623,9 +32260,11 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
31623
32260
  viewport_height: exports_external.number().optional().default(720),
31624
32261
  stealth: exports_external.boolean().optional().default(false),
31625
32262
  auto_gallery: exports_external.boolean().optional().default(false),
32263
+ storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
31626
32264
  force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
31627
- tags: exports_external.array(exports_external.string()).optional()
31628
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth, auto_gallery, force_new, tags }) => {
32265
+ tags: exports_external.array(exports_external.string()).optional(),
32266
+ cdp_url: exports_external.string().optional().describe("Connect to existing Chrome via CDP (e.g. http://localhost:9222). Start Chrome with --remote-debugging-port=9222")
32267
+ }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth, auto_gallery, storage_state, force_new, tags, cdp_url }) => {
31629
32268
  try {
31630
32269
  if (agent_id && !force_new) {
31631
32270
  const existing = getActiveSessionForAgent2(agent_id);
@@ -31641,7 +32280,9 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
31641
32280
  headless,
31642
32281
  viewport: { width: viewport_width, height: viewport_height },
31643
32282
  stealth,
31644
- autoGallery: auto_gallery
32283
+ autoGallery: auto_gallery,
32284
+ storageState: storage_state,
32285
+ cdpUrl: cdp_url
31645
32286
  });
31646
32287
  if (tags?.length) {
31647
32288
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -31801,7 +32442,7 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
31801
32442
  return err(e);
31802
32443
  }
31803
32444
  });
31804
- server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string().optional(), 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 }) => {
32445
+ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability. Self-healing auto-tries fallback selectors if element not found.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional(), self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found") }, async ({ session_id, selector, ref, button, timeout, self_heal }) => {
31805
32446
  try {
31806
32447
  const sid = resolveSessionId(session_id);
31807
32448
  const page = getSessionPage(sid);
@@ -31812,14 +32453,17 @@ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS sel
31812
32453
  }
31813
32454
  if (!selector)
31814
32455
  return err(new Error("Either ref or selector is required"));
31815
- await click(page, selector, { button, timeout });
31816
- logEvent(sid, "click", { selector, method: "selector" });
32456
+ const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
32457
+ logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
32458
+ if (healInfo.healed) {
32459
+ return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
32460
+ }
31817
32461
  return json({ clicked: selector, method: "selector" });
31818
32462
  } catch (e) {
31819
32463
  return errWithScreenshot(e, session_id);
31820
32464
  }
31821
32465
  });
31822
- server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string().optional(), 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 }) => {
32466
+ server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref. Self-healing auto-tries fallback selectors if element not found.", { session_id: exports_external.string().optional(), 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(), self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found") }, async ({ session_id, selector, ref, text, clear, delay, self_heal }) => {
31823
32467
  try {
31824
32468
  const sid = resolveSessionId(session_id);
31825
32469
  const page = getSessionPage(sid);
@@ -31830,8 +32474,11 @@ server.tool("browser_type", "Type text into an element by ref or selector. Prefe
31830
32474
  }
31831
32475
  if (!selector)
31832
32476
  return err(new Error("Either ref or selector is required"));
31833
- await type(page, selector, text, { clear, delay });
31834
- logEvent(sid, "type", { selector, text: text.slice(0, 100) });
32477
+ const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
32478
+ logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
32479
+ if (healInfo.healed) {
32480
+ return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
32481
+ }
31835
32482
  return json({ typed: text, selector, method: "selector" });
31836
32483
  } catch (e) {
31837
32484
  return errWithScreenshot(e, session_id);
@@ -31925,20 +32572,32 @@ server.tool("browser_wait", "Wait for a selector to appear", { session_id: expor
31925
32572
  return err(e);
31926
32573
  }
31927
32574
  });
31928
- server.tool("browser_get_text", "Get text content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
32575
+ server.tool("browser_get_text", "Get text content from the page or a selector. Sanitizes prompt injection by default.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from text (default: true)") }, async ({ session_id, selector, sanitize }) => {
31929
32576
  try {
31930
32577
  const sid = resolveSessionId(session_id);
31931
32578
  const page = getSessionPage(sid);
31932
- return json({ text: await getText(page, selector) });
32579
+ const text = await getText(page, selector);
32580
+ if (sanitize) {
32581
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
32582
+ const sanitized = sanitizeText2(text);
32583
+ return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
32584
+ }
32585
+ return json({ text });
31933
32586
  } catch (e) {
31934
32587
  return err(e);
31935
32588
  }
31936
32589
  });
31937
- server.tool("browser_get_html", "Get HTML content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
32590
+ server.tool("browser_get_html", "Get HTML content from the page or a selector. Sanitizes prompt injection by default.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns and hidden elements from HTML (default: true)") }, async ({ session_id, selector, sanitize }) => {
31938
32591
  try {
31939
32592
  const sid = resolveSessionId(session_id);
31940
32593
  const page = getSessionPage(sid);
31941
- return json({ html: await getHTML(page, selector) });
32594
+ const html = await getHTML(page, selector);
32595
+ if (sanitize) {
32596
+ const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
32597
+ const sanitized = sanitizeHTML2(html);
32598
+ return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
32599
+ }
32600
+ return json({ html });
31942
32601
  } catch (e) {
31943
32602
  return err(e);
31944
32603
  }
@@ -31953,16 +32612,32 @@ server.tool("browser_get_links", "Get all links from the current page", { sessio
31953
32612
  return err(e);
31954
32613
  }
31955
32614
  });
31956
- server.tool("browser_extract", "Extract content from the page in a specified format", {
32615
+ server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
31957
32616
  session_id: exports_external.string().optional(),
31958
32617
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
31959
32618
  selector: exports_external.string().optional(),
31960
- schema: exports_external.record(exports_external.string()).optional()
31961
- }, async ({ session_id, format, selector, schema }) => {
32619
+ schema: exports_external.record(exports_external.string()).optional(),
32620
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
32621
+ }, async ({ session_id, format, selector, schema, sanitize }) => {
31962
32622
  try {
31963
32623
  const sid = resolveSessionId(session_id);
31964
32624
  const page = getSessionPage(sid);
31965
32625
  const result = await extract(page, { format, selector, schema });
32626
+ if (sanitize) {
32627
+ const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
32628
+ if (result.text) {
32629
+ const s = sanitizeText2(result.text);
32630
+ result.text = s.text;
32631
+ result.stripped = s.stripped;
32632
+ result.warnings = s.warnings;
32633
+ }
32634
+ if (result.html) {
32635
+ const s = sanitizeHTML2(result.html);
32636
+ result.html = s.text;
32637
+ result.stripped = s.stripped;
32638
+ result.warnings = s.warnings;
32639
+ }
32640
+ }
31966
32641
  return json(result);
31967
32642
  } catch (e) {
31968
32643
  return err(e);
@@ -31979,17 +32654,27 @@ server.tool("browser_find", "Find elements matching a selector and return their
31979
32654
  return err(e);
31980
32655
  }
31981
32656
  });
31982
- 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.", {
32657
+ 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. Sanitizes prompt injection by default.", {
31983
32658
  session_id: exports_external.string().optional(),
31984
32659
  compact: exports_external.boolean().optional().default(true),
31985
32660
  max_refs: exports_external.number().optional().default(50),
31986
- full_tree: exports_external.boolean().optional().default(false)
31987
- }, async ({ session_id, compact, max_refs, full_tree }) => {
32661
+ full_tree: exports_external.boolean().optional().default(false),
32662
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
32663
+ }, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
31988
32664
  try {
31989
32665
  const sid = resolveSessionId(session_id);
31990
32666
  const page = getSessionPage(sid);
31991
32667
  const result = await takeSnapshot(page, sid);
31992
32668
  setLastSnapshot(sid, result);
32669
+ let injection_warnings;
32670
+ if (sanitize) {
32671
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
32672
+ const sanitized = sanitizeText2(result.tree);
32673
+ if (sanitized.stripped > 0) {
32674
+ injection_warnings = sanitized.warnings;
32675
+ result.tree = sanitized.text;
32676
+ }
32677
+ }
31993
32678
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
31994
32679
  const limitedRefs = Object.fromEntries(refEntries);
31995
32680
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -32001,27 +32686,29 @@ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@
32001
32686
  interactive_count: result.interactive_count,
32002
32687
  shown_count: refEntries.length,
32003
32688
  truncated,
32004
- refs: limitedRefs
32689
+ refs: limitedRefs,
32690
+ ...injection_warnings ? { injection_warnings } : {}
32005
32691
  });
32006
32692
  }
32007
32693
  const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
32008
32694
  ... (truncated \u2014 use full_tree=true for complete)` : "");
32009
- return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
32695
+ return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
32010
32696
  } catch (e) {
32011
32697
  return err(e);
32012
32698
  }
32013
32699
  });
32014
- server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
32700
+ server.tool("browser_screenshot", "Take a screenshot. Use selector to capture a specific element/section instead of the full page. Use detail='high' for AI-readable full image, 'low' for fast thumbnail. Use annotate=true to overlay numbered labels on interactive elements.", {
32015
32701
  session_id: exports_external.string().optional(),
32016
- selector: exports_external.string().optional(),
32702
+ selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
32017
32703
  full_page: exports_external.boolean().optional().default(false),
32018
32704
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
32019
32705
  quality: exports_external.number().optional().default(60),
32020
32706
  max_width: exports_external.number().optional().default(800),
32021
32707
  compress: exports_external.boolean().optional().default(true),
32022
32708
  thumbnail: exports_external.boolean().optional().default(true),
32023
- annotate: exports_external.boolean().optional().default(false)
32024
- }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
32709
+ annotate: exports_external.boolean().optional().default(false),
32710
+ detail: exports_external.enum(["low", "high"]).optional().default("low").describe("'low' = thumbnail only (fast, saves tokens). 'high' = full readable image in base64 (larger but AI can read text).")
32711
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
32025
32712
  try {
32026
32713
  const sid = resolveSessionId(session_id);
32027
32714
  const page = getSessionPage(sid);
@@ -32038,7 +32725,9 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
32038
32725
  annotation_count: annotated.annotations.length
32039
32726
  });
32040
32727
  }
32041
- const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
32728
+ const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
32729
+ const effectiveQuality = detail === "high" ? 75 : quality;
32730
+ const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
32042
32731
  result.url = page.url();
32043
32732
  try {
32044
32733
  const buf = Buffer.from(result.base64, "base64");
@@ -32047,12 +32736,12 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
32047
32736
  result.download_id = dl.id;
32048
32737
  } catch {}
32049
32738
  result.estimated_tokens = Math.ceil(result.base64.length / 4);
32050
- if (result.base64.length > 20000) {
32739
+ if (detail !== "high" && result.base64.length > 40000) {
32051
32740
  result.base64_truncated = true;
32052
32741
  result.full_image_path = result.path;
32053
32742
  result.base64 = result.thumbnail_base64 ?? "";
32054
32743
  }
32055
- logEvent(sid, "screenshot", { path: result.path });
32744
+ logEvent(sid, "screenshot", { path: result.path, detail, selector });
32056
32745
  return json(result);
32057
32746
  } catch (e) {
32058
32747
  return err(e);
@@ -32458,6 +33147,94 @@ server.tool("browser_session_untag", "Remove a tag from a session", { session_id
32458
33147
  return err(e);
32459
33148
  }
32460
33149
  });
33150
+ server.tool("browser_session_save_state", "Save current session's auth state (cookies, localStorage) for reuse. Use after login to avoid re-authenticating.", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Name for this state (e.g. 'github', 'gmail')") }, async ({ session_id, name }) => {
33151
+ try {
33152
+ const sid = resolveSessionId(session_id);
33153
+ const page = getSessionPage(sid);
33154
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33155
+ const path = await saveStateFromPage2(page, name);
33156
+ return json({ saved: true, name, path });
33157
+ } catch (e) {
33158
+ return err(e);
33159
+ }
33160
+ });
33161
+ server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
33162
+ try {
33163
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33164
+ const states = listStates2();
33165
+ return json({ states, count: states.length });
33166
+ } catch (e) {
33167
+ return err(e);
33168
+ }
33169
+ });
33170
+ server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
33171
+ try {
33172
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33173
+ return json({ deleted: deleteState2(name), name });
33174
+ } catch (e) {
33175
+ return err(e);
33176
+ }
33177
+ });
33178
+ server.tool("browser_auth_record", "Start recording a login flow. Navigate to the login page, perform the login, then call browser_auth_stop to save.", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Name for this auth flow (e.g. 'github', 'gmail')"), start_url: exports_external.string().optional().describe("Login page URL") }, async ({ session_id, name, start_url }) => {
33179
+ try {
33180
+ const sid = resolveSessionId(session_id);
33181
+ const page = getSessionPage(sid);
33182
+ if (start_url)
33183
+ await navigate(page, start_url);
33184
+ const recording = startRecording(sid, `auth-${name}`, page.url());
33185
+ return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
33186
+ } catch (e) {
33187
+ return err(e);
33188
+ }
33189
+ });
33190
+ server.tool("browser_auth_stop", "Stop recording a login flow and save as a reusable auth flow with storage state.", { session_id: exports_external.string().optional(), name: exports_external.string(), recording_id: exports_external.string() }, async ({ session_id, name, recording_id }) => {
33191
+ try {
33192
+ const sid = resolveSessionId(session_id);
33193
+ const page = getSessionPage(sid);
33194
+ const recording = stopRecording(recording_id);
33195
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33196
+ const statePath2 = await saveStateFromPage2(page, name);
33197
+ let domain = "";
33198
+ try {
33199
+ domain = new URL(page.url()).hostname;
33200
+ } catch {}
33201
+ const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33202
+ const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
33203
+ return json({ flow, recording_steps: recording.steps.length });
33204
+ } catch (e) {
33205
+ return err(e);
33206
+ }
33207
+ });
33208
+ server.tool("browser_auth_replay", "Manually replay a saved auth flow for a domain", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Auth flow name to replay") }, async ({ session_id, name }) => {
33209
+ try {
33210
+ const sid = resolveSessionId(session_id);
33211
+ const page = getSessionPage(sid);
33212
+ const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33213
+ const flow = getAuthFlowByName2(name);
33214
+ if (!flow)
33215
+ return err(new Error(`Auth flow '${name}' not found`));
33216
+ const result = await tryReplayAuth2(page, flow.domain);
33217
+ return json(result);
33218
+ } catch (e) {
33219
+ return err(e);
33220
+ }
33221
+ });
33222
+ server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
33223
+ try {
33224
+ const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33225
+ return json({ flows: listAuthFlows2() });
33226
+ } catch (e) {
33227
+ return err(e);
33228
+ }
33229
+ });
33230
+ server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
33231
+ try {
33232
+ const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33233
+ return json({ deleted: deleteAuthFlow2(name) });
33234
+ } catch (e) {
33235
+ return err(e);
33236
+ }
33237
+ });
32461
33238
  server.tool("browser_click_text", "Click an element by its visible text content", { session_id: exports_external.string().optional(), text: exports_external.string(), exact: exports_external.boolean().optional().default(false), timeout: exports_external.number().optional() }, async ({ session_id, text, exact, timeout }) => {
32462
33239
  try {
32463
33240
  const sid = resolveSessionId(session_id);
@@ -32468,20 +33245,45 @@ server.tool("browser_click_text", "Click an element by its visible text content"
32468
33245
  return err(e);
32469
33246
  }
32470
33247
  });
32471
- server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
33248
+ server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects. Self-healing auto-tries fallback selectors per field.", {
32472
33249
  session_id: exports_external.string().optional(),
32473
33250
  fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
32474
- submit_selector: exports_external.string().optional()
32475
- }, async ({ session_id, fields, submit_selector }) => {
33251
+ submit_selector: exports_external.string().optional(),
33252
+ self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
33253
+ }, async ({ session_id, fields, submit_selector, self_heal }) => {
32476
33254
  try {
32477
33255
  const sid = resolveSessionId(session_id);
32478
33256
  const page = getSessionPage(sid);
32479
- const result = await fillForm(page, fields, submit_selector);
33257
+ const result = await fillForm(page, fields, submit_selector, self_heal);
32480
33258
  return json(result);
32481
33259
  } catch (e) {
32482
33260
  return errWithScreenshot(e, session_id);
32483
33261
  }
32484
33262
  });
33263
+ server.tool("browser_find_visual", "Find an element using AI vision when selectors and a11y refs fail. Useful for canvas, images, custom widgets. Takes a screenshot and asks a vision model to locate the element.", {
33264
+ session_id: exports_external.string().optional(),
33265
+ description: exports_external.string().describe("Natural language description of the element to find (e.g. 'the blue Submit button', 'the search icon in the top right')"),
33266
+ click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
33267
+ model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
33268
+ }, async ({ session_id, description, click: doClick, model }) => {
33269
+ try {
33270
+ const sid = resolveSessionId(session_id);
33271
+ const page = getSessionPage(sid);
33272
+ if (doClick) {
33273
+ const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
33274
+ const result = await clickByVision2(page, description, { model });
33275
+ logEvent(sid, "vision_click", { query: description, ...result });
33276
+ return json(result);
33277
+ } else {
33278
+ const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
33279
+ const result = await findElementByVision2(page, description, { model });
33280
+ logEvent(sid, "vision_find", { query: description, ...result });
33281
+ return json(result);
33282
+ }
33283
+ } catch (e) {
33284
+ return err(e);
33285
+ }
33286
+ });
32485
33287
  server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string().optional(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
32486
33288
  try {
32487
33289
  const sid = resolveSessionId(session_id);
@@ -32902,6 +33704,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32902
33704
  { tool: "browser_wait", description: "Wait for a selector to appear" },
32903
33705
  { tool: "browser_wait_for_text", description: "Wait for text to appear" },
32904
33706
  { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
33707
+ { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
32905
33708
  { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
32906
33709
  ],
32907
33710
  Extraction: [
@@ -32930,7 +33733,10 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32930
33733
  { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
32931
33734
  { tool: "browser_profile_load", description: "Load and apply a saved profile" },
32932
33735
  { tool: "browser_profile_list", description: "List saved profiles" },
32933
- { tool: "browser_profile_delete", description: "Delete a saved profile" }
33736
+ { tool: "browser_profile_delete", description: "Delete a saved profile" },
33737
+ { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
33738
+ { tool: "browser_session_list_states", description: "List saved storage states" },
33739
+ { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
32934
33740
  ],
32935
33741
  Network: [
32936
33742
  { tool: "browser_network_log", description: "Get captured network requests" },
@@ -32954,6 +33760,13 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32954
33760
  { tool: "browser_record_replay", description: "Replay a recorded sequence" },
32955
33761
  { tool: "browser_recordings_list", description: "List all recordings" }
32956
33762
  ],
33763
+ Auth: [
33764
+ { tool: "browser_auth_record", description: "Start recording a login flow" },
33765
+ { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
33766
+ { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
33767
+ { tool: "browser_auth_list", description: "List all saved auth flows" },
33768
+ { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
33769
+ ],
32957
33770
  Crawl: [
32958
33771
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
32959
33772
  ],
@@ -33010,7 +33823,8 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33010
33823
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
33011
33824
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
33012
33825
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
33013
- { tool: "browser_watch_stop", description: "Stop DOM watcher" }
33826
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" },
33827
+ { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
33014
33828
  ]
33015
33829
  };
33016
33830
  const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
@@ -33293,6 +34107,82 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
33293
34107
  return err(e);
33294
34108
  }
33295
34109
  });
34110
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
34111
+ actions: exports_external.array(exports_external.object({
34112
+ session_id: exports_external.string().describe("Target session ID"),
34113
+ tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
34114
+ args: exports_external.record(exports_external.unknown()).optional().default({})
34115
+ })),
34116
+ timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
34117
+ }, async ({ actions, timeout }) => {
34118
+ try {
34119
+ const t0 = Date.now();
34120
+ const promises = actions.map(async (action, index) => {
34121
+ try {
34122
+ const sid = action.session_id;
34123
+ const page = getSessionPage(sid);
34124
+ const args = action.args;
34125
+ const toolName = action.tool.replace(/^browser_/, "");
34126
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
34127
+ const actionPromise = (async () => {
34128
+ switch (toolName) {
34129
+ case "navigate": {
34130
+ await navigate(page, args.url);
34131
+ const title = await page.title();
34132
+ return { url: page.url(), title };
34133
+ }
34134
+ case "screenshot": {
34135
+ const result2 = await takeScreenshot(page, {
34136
+ maxWidth: args.max_width ?? 800,
34137
+ quality: args.quality ?? 60
34138
+ });
34139
+ return { path: result2.path, size_bytes: result2.size_bytes };
34140
+ }
34141
+ case "click": {
34142
+ if (args.selector)
34143
+ await click(page, args.selector);
34144
+ return { clicked: args.selector };
34145
+ }
34146
+ case "type": {
34147
+ if (args.selector && args.text)
34148
+ await type(page, args.selector, args.text);
34149
+ return { typed: args.text };
34150
+ }
34151
+ case "get_text": {
34152
+ const text = await getText(page);
34153
+ return { text: text.slice(0, 1000), length: text.length };
34154
+ }
34155
+ case "get_links": {
34156
+ const links = await getLinks(page);
34157
+ return { links, count: links.length };
34158
+ }
34159
+ case "snapshot": {
34160
+ const snap = await takeSnapshot(page, sid);
34161
+ return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
34162
+ }
34163
+ case "evaluate": {
34164
+ const result2 = await page.evaluate(args.expression);
34165
+ return { result: result2 };
34166
+ }
34167
+ default:
34168
+ return { error: `Unknown tool: ${action.tool}` };
34169
+ }
34170
+ })();
34171
+ const result = await Promise.race([actionPromise, timeoutPromise]);
34172
+ return { index, session_id: sid, tool: action.tool, success: true, result };
34173
+ } catch (e) {
34174
+ return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
34175
+ }
34176
+ });
34177
+ const results = await Promise.all(promises);
34178
+ const duration_ms = Date.now() - t0;
34179
+ const succeeded = results.filter((r) => r.success).length;
34180
+ const failed = results.filter((r) => !r.success).length;
34181
+ return json({ results, duration_ms, succeeded, failed, total: actions.length });
34182
+ } catch (e) {
34183
+ return err(e);
34184
+ }
34185
+ });
33296
34186
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
33297
34187
  try {
33298
34188
  return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });