@hasna/browser 0.0.9 → 0.1.1

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,71 @@ 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
+ `
327
+ },
328
+ {
329
+ version: 7,
330
+ sql: `
331
+ CREATE TABLE IF NOT EXISTS workflows (
332
+ id TEXT PRIMARY KEY,
333
+ name TEXT NOT NULL UNIQUE,
334
+ description TEXT,
335
+ steps TEXT NOT NULL DEFAULT '[]',
336
+ start_url TEXT,
337
+ last_run TEXT,
338
+ last_heal TEXT,
339
+ heal_count INTEGER DEFAULT 0,
340
+ run_count INTEGER DEFAULT 0,
341
+ created_at TEXT DEFAULT (datetime('now')),
342
+ updated_at TEXT DEFAULT (datetime('now'))
343
+ );
344
+ `
345
+ },
346
+ {
347
+ version: 8,
348
+ sql: `
349
+ CREATE TABLE IF NOT EXISTS datasets (
350
+ id TEXT PRIMARY KEY,
351
+ name TEXT NOT NULL UNIQUE,
352
+ source_url TEXT,
353
+ source_type TEXT NOT NULL DEFAULT 'page',
354
+ data TEXT NOT NULL DEFAULT '[]',
355
+ schema TEXT,
356
+ row_count INTEGER DEFAULT 0,
357
+ last_refresh TEXT,
358
+ created_at TEXT DEFAULT (datetime('now')),
359
+ updated_at TEXT DEFAULT (datetime('now'))
360
+ );
361
+
362
+ CREATE TABLE IF NOT EXISTS api_endpoints (
363
+ id TEXT PRIMARY KEY,
364
+ session_id TEXT,
365
+ url TEXT NOT NULL,
366
+ method TEXT DEFAULT 'GET',
367
+ response_schema TEXT,
368
+ sample_response TEXT,
369
+ status_code INTEGER,
370
+ content_type TEXT,
371
+ discovered_at TEXT DEFAULT (datetime('now'))
372
+ );
373
+ CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
374
+ `
310
375
  }
311
376
  ];
312
377
  for (const m of migrations) {
@@ -1267,14 +1332,14 @@ function enableConsoleCapture(page, sessionId) {
1267
1332
  warning: "warn"
1268
1333
  };
1269
1334
  const level = levelMap[msg.type()] ?? "log";
1270
- const location = msg.location();
1335
+ const location2 = msg.location();
1271
1336
  try {
1272
1337
  logConsoleMessage({
1273
1338
  session_id: sessionId,
1274
1339
  level,
1275
1340
  message: msg.text(),
1276
- source: location.url || undefined,
1277
- line_number: location.lineNumber || undefined
1341
+ source: location2.url || undefined,
1342
+ line_number: location2.lineNumber || undefined
1278
1343
  });
1279
1344
  } catch {}
1280
1345
  };
@@ -1418,6 +1483,188 @@ var init_dialogs = __esm(() => {
1418
1483
  pendingDialogs = new Map;
1419
1484
  });
1420
1485
 
1486
+ // src/engines/cdp.ts
1487
+ var exports_cdp = {};
1488
+ __export(exports_cdp, {
1489
+ connectToExistingBrowser: () => connectToExistingBrowser,
1490
+ CDPClient: () => CDPClient
1491
+ });
1492
+ async function connectToExistingBrowser(cdpUrl) {
1493
+ const { chromium: chromium3 } = await import("playwright");
1494
+ try {
1495
+ return await chromium3.connectOverCDP(cdpUrl);
1496
+ } catch (err) {
1497
+ 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);
1498
+ }
1499
+ }
1500
+
1501
+ class CDPClient {
1502
+ session;
1503
+ networkEnabled = false;
1504
+ performanceEnabled = false;
1505
+ constructor(session) {
1506
+ this.session = session;
1507
+ }
1508
+ static async fromPage(page) {
1509
+ try {
1510
+ const session = await page.context().newCDPSession(page);
1511
+ return new CDPClient(session);
1512
+ } catch (err) {
1513
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
1514
+ }
1515
+ }
1516
+ async send(method, params) {
1517
+ try {
1518
+ return await this.session.send(method, params);
1519
+ } catch (err) {
1520
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
1521
+ }
1522
+ }
1523
+ on(event, handler) {
1524
+ this.session.on(event, handler);
1525
+ }
1526
+ off(event, handler) {
1527
+ this.session.off(event, handler);
1528
+ }
1529
+ async enableNetwork() {
1530
+ if (!this.networkEnabled) {
1531
+ await this.send("Network.enable");
1532
+ this.networkEnabled = true;
1533
+ }
1534
+ }
1535
+ async enablePerformance() {
1536
+ if (!this.performanceEnabled) {
1537
+ await this.send("Performance.enable");
1538
+ this.performanceEnabled = true;
1539
+ }
1540
+ }
1541
+ async getPerformanceMetrics() {
1542
+ await this.enablePerformance();
1543
+ const result = await this.send("Performance.getMetrics");
1544
+ const m = {};
1545
+ for (const metric of result.metrics) {
1546
+ m[metric.name] = metric.value;
1547
+ }
1548
+ return {
1549
+ js_heap_size_used: m["JSHeapUsedSize"],
1550
+ js_heap_size_total: m["JSHeapTotalSize"],
1551
+ dom_interactive: m["DOMInteractive"],
1552
+ dom_complete: m["DOMComplete"],
1553
+ load_event: m["LoadEventEnd"]
1554
+ };
1555
+ }
1556
+ async startJSCoverage() {
1557
+ await this.send("Profiler.enable");
1558
+ await this.send("Debugger.enable");
1559
+ await this.send("Profiler.startPreciseCoverage", {
1560
+ callCount: false,
1561
+ detailed: true
1562
+ });
1563
+ }
1564
+ async stopJSCoverage() {
1565
+ const result = await this.send("Profiler.takePreciseCoverage");
1566
+ await this.send("Profiler.stopPreciseCoverage");
1567
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
1568
+ url: r.url,
1569
+ text: "",
1570
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
1571
+ }));
1572
+ }
1573
+ async getCoverage() {
1574
+ await this.startJSCoverage();
1575
+ const js = await this.stopJSCoverage();
1576
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
1577
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
1578
+ }
1579
+ async captureHAREntries(page, handler) {
1580
+ await this.enableNetwork();
1581
+ const requestTimings = new Map;
1582
+ const onRequest = (params) => {
1583
+ requestTimings.set(params.requestId, params.timestamp);
1584
+ };
1585
+ const onResponse = (params) => {
1586
+ const start = requestTimings.get(params.requestId);
1587
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
1588
+ handler({
1589
+ method: "GET",
1590
+ url: params.response.url,
1591
+ status: params.response.status,
1592
+ duration
1593
+ });
1594
+ };
1595
+ this.on("Network.requestWillBeSent", onRequest);
1596
+ this.on("Network.responseReceived", onResponse);
1597
+ return () => {
1598
+ this.off("Network.requestWillBeSent", onRequest);
1599
+ this.off("Network.responseReceived", onResponse);
1600
+ };
1601
+ }
1602
+ async detach() {
1603
+ try {
1604
+ await this.session.detach();
1605
+ } catch {}
1606
+ }
1607
+ }
1608
+ var init_cdp = __esm(() => {
1609
+ init_types();
1610
+ });
1611
+
1612
+ // src/lib/storage-state.ts
1613
+ var exports_storage_state = {};
1614
+ __export(exports_storage_state, {
1615
+ saveStateFromPage: () => saveStateFromPage,
1616
+ saveState: () => saveState,
1617
+ loadStatePath: () => loadStatePath,
1618
+ listStates: () => listStates,
1619
+ deleteState: () => deleteState
1620
+ });
1621
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
1622
+ import { join as join3 } from "path";
1623
+ import { homedir as homedir3 } from "os";
1624
+ function ensureDir() {
1625
+ mkdirSync3(STATES_DIR, { recursive: true });
1626
+ }
1627
+ function statePath(name) {
1628
+ return join3(STATES_DIR, `${name}.json`);
1629
+ }
1630
+ async function saveState(context, name) {
1631
+ ensureDir();
1632
+ const path = statePath(name);
1633
+ const state = await context.storageState({ path });
1634
+ return path;
1635
+ }
1636
+ async function saveStateFromPage(page, name) {
1637
+ return saveState(page.context(), name);
1638
+ }
1639
+ function loadStatePath(name) {
1640
+ const path = statePath(name);
1641
+ return existsSync(path) ? path : null;
1642
+ }
1643
+ function listStates() {
1644
+ ensureDir();
1645
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
1646
+ const path = join3(STATES_DIR, f);
1647
+ const stat = Bun.file(path);
1648
+ return {
1649
+ name: f.replace(".json", ""),
1650
+ path,
1651
+ modified: new Date(stat.lastModified).toISOString()
1652
+ };
1653
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
1654
+ }
1655
+ function deleteState(name) {
1656
+ const path = statePath(name);
1657
+ if (existsSync(path)) {
1658
+ unlinkSync(path);
1659
+ return true;
1660
+ }
1661
+ return false;
1662
+ }
1663
+ var STATES_DIR;
1664
+ var init_storage_state = __esm(() => {
1665
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
1666
+ });
1667
+
1421
1668
  // src/lib/session.ts
1422
1669
  var exports_session = {};
1423
1670
  __export(exports_session, {
@@ -1447,6 +1694,37 @@ function createBunProxy(view) {
1447
1694
  return view;
1448
1695
  }
1449
1696
  async function createSession2(opts = {}) {
1697
+ if (opts.cdpUrl) {
1698
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
1699
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
1700
+ const contexts = cdpBrowser.contexts();
1701
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
1702
+ const pages = context.pages();
1703
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
1704
+ const session2 = createSession({
1705
+ engine: "cdp",
1706
+ projectId: opts.projectId,
1707
+ agentId: opts.agentId,
1708
+ startUrl: page2.url(),
1709
+ name: opts.name ?? "attached"
1710
+ });
1711
+ const cleanups2 = [];
1712
+ if (opts.captureNetwork !== false) {
1713
+ try {
1714
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
1715
+ } catch {}
1716
+ }
1717
+ if (opts.captureConsole !== false) {
1718
+ try {
1719
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
1720
+ } catch {}
1721
+ }
1722
+ try {
1723
+ cleanups2.push(setupDialogHandler(page2, session2.id));
1724
+ } catch {}
1725
+ 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 });
1726
+ return { session: session2, page: page2 };
1727
+ }
1450
1728
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
1451
1729
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
1452
1730
  let browser = null;
@@ -1472,7 +1750,22 @@ async function createSession2(opts = {}) {
1472
1750
  page = await context.newPage();
1473
1751
  } else {
1474
1752
  browser = await pool.acquire(opts.headless ?? true);
1475
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1753
+ if (opts.storageState) {
1754
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
1755
+ const statePath2 = loadStatePath2(opts.storageState);
1756
+ if (statePath2) {
1757
+ const context = await browser.newContext({
1758
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
1759
+ userAgent: opts.userAgent,
1760
+ storageState: statePath2
1761
+ });
1762
+ page = await context.newPage();
1763
+ } else {
1764
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1765
+ }
1766
+ } else {
1767
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
1768
+ }
1476
1769
  }
1477
1770
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
1478
1771
  try {
@@ -1977,6 +2270,66 @@ var init_snapshot = __esm(() => {
1977
2270
  ];
1978
2271
  });
1979
2272
 
2273
+ // src/lib/self-heal.ts
2274
+ async function healSelector(page, selector, sessionId) {
2275
+ const attempts = [];
2276
+ attempts.push(`selector: ${selector}`);
2277
+ try {
2278
+ const loc = page.locator(selector).first();
2279
+ if (await loc.count() > 0) {
2280
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
2281
+ }
2282
+ } catch {}
2283
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
2284
+ attempts.push(`text: "${selector}"`);
2285
+ try {
2286
+ const loc = page.getByText(selector, { exact: false }).first();
2287
+ if (await loc.count() > 0) {
2288
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
2289
+ }
2290
+ } catch {}
2291
+ }
2292
+ const roleMap = {
2293
+ button: ["button", "submit", "reset"],
2294
+ link: ["a"],
2295
+ input: ["input", "textarea"],
2296
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
2297
+ };
2298
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
2299
+ for (const [role, tags] of Object.entries(roleMap)) {
2300
+ attempts.push(`role: ${role} name~="${nameHint}"`);
2301
+ try {
2302
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
2303
+ if (await loc.count() > 0) {
2304
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
2305
+ }
2306
+ } catch {}
2307
+ }
2308
+ if (selector.startsWith("#")) {
2309
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
2310
+ const partialSel = `[id*="${idPart}"]`;
2311
+ attempts.push(`partial_id: ${partialSel}`);
2312
+ try {
2313
+ const loc = page.locator(partialSel).first();
2314
+ if (await loc.count() > 0) {
2315
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
2316
+ }
2317
+ } catch {}
2318
+ }
2319
+ if (selector.startsWith(".")) {
2320
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
2321
+ const partialSel = `[class*="${classPart}"]`;
2322
+ attempts.push(`partial_class: ${partialSel}`);
2323
+ try {
2324
+ const loc = page.locator(partialSel).first();
2325
+ if (await loc.count() > 0) {
2326
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
2327
+ }
2328
+ } catch {}
2329
+ }
2330
+ return { found: false, locator: null, method: "none", healed: false, attempts };
2331
+ }
2332
+
1980
2333
  // src/lib/actions.ts
1981
2334
  var exports_actions = {};
1982
2335
  __export(exports_actions, {
@@ -2018,11 +2371,22 @@ async function click(page, selector, opts) {
2018
2371
  delay: opts?.delay,
2019
2372
  timeout: opts?.timeout ?? 1e4
2020
2373
  });
2021
- } catch (err) {
2022
- if (err instanceof Error && err.message.includes("not found")) {
2374
+ return {};
2375
+ } catch (originalError) {
2376
+ if (opts?.selfHeal !== false) {
2377
+ const result = await healSelector(page, selector);
2378
+ if (result.found && result.locator) {
2379
+ await result.locator.click({
2380
+ button: opts?.button ?? "left",
2381
+ timeout: opts?.timeout ?? 1e4
2382
+ });
2383
+ return { healed: true, method: result.method, attempts: result.attempts };
2384
+ }
2385
+ }
2386
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
2023
2387
  throw new ElementNotFoundError(selector);
2024
2388
  }
2025
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
2389
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
2026
2390
  }
2027
2391
  }
2028
2392
  async function type(page, selector, text, opts) {
@@ -2031,17 +2395,35 @@ async function type(page, selector, text, opts) {
2031
2395
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
2032
2396
  }
2033
2397
  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")) {
2398
+ return {};
2399
+ } catch (originalError) {
2400
+ if (opts?.selfHeal !== false) {
2401
+ const result = await healSelector(page, selector);
2402
+ if (result.found && result.locator) {
2403
+ if (opts?.clear)
2404
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
2405
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
2406
+ return { healed: true, method: result.method, attempts: result.attempts };
2407
+ }
2408
+ }
2409
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
2036
2410
  throw new ElementNotFoundError(selector);
2037
2411
  }
2038
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
2412
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
2039
2413
  }
2040
2414
  }
2041
- async function fill(page, selector, value, timeout = 1e4) {
2415
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
2042
2416
  try {
2043
2417
  await page.fill(selector, value, { timeout });
2044
- } catch (err) {
2418
+ return {};
2419
+ } catch (originalError) {
2420
+ if (selfHeal) {
2421
+ const result = await healSelector(page, selector);
2422
+ if (result.found && result.locator) {
2423
+ await result.locator.fill(value, { timeout });
2424
+ return { healed: true, method: result.method, attempts: result.attempts };
2425
+ }
2426
+ }
2045
2427
  throw new ElementNotFoundError(selector);
2046
2428
  }
2047
2429
  }
@@ -2166,12 +2548,39 @@ async function clickText(page, text, opts) {
2166
2548
  }
2167
2549
  }, { retries: opts?.retries ?? 1 });
2168
2550
  }
2169
- async function fillForm(page, fields, submitSelector) {
2551
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
2170
2552
  let filled = 0;
2171
2553
  const errors2 = [];
2554
+ const healedFields = [];
2172
2555
  for (const [selector, value] of Object.entries(fields)) {
2173
2556
  try {
2174
- const el = await page.$(selector);
2557
+ let el = await page.$(selector);
2558
+ if (!el && selfHeal) {
2559
+ const result = await healSelector(page, selector);
2560
+ if (result.found && result.locator) {
2561
+ const handle = await result.locator.elementHandle();
2562
+ if (handle) {
2563
+ el = handle;
2564
+ healedFields.push(selector);
2565
+ const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
2566
+ const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
2567
+ if (tagName2 === "select") {
2568
+ await result.locator.selectOption(String(value));
2569
+ } else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
2570
+ if (Boolean(value))
2571
+ await result.locator.check();
2572
+ else
2573
+ await result.locator.uncheck();
2574
+ } else {
2575
+ await result.locator.fill(String(value));
2576
+ }
2577
+ filled++;
2578
+ continue;
2579
+ }
2580
+ }
2581
+ errors2.push(`${selector}: element not found`);
2582
+ continue;
2583
+ }
2175
2584
  if (!el) {
2176
2585
  errors2.push(`${selector}: element not found`);
2177
2586
  continue;
@@ -2198,11 +2607,21 @@ async function fillForm(page, fields, submitSelector) {
2198
2607
  if (submitSelector) {
2199
2608
  try {
2200
2609
  await page.click(submitSelector);
2201
- } catch (err) {
2202
- errors2.push(`submit(${submitSelector}): ${err instanceof Error ? err.message : String(err)}`);
2610
+ } catch (submitErr) {
2611
+ if (selfHeal) {
2612
+ const result = await healSelector(page, submitSelector);
2613
+ if (result.found && result.locator) {
2614
+ await result.locator.click();
2615
+ healedFields.push(submitSelector);
2616
+ } else {
2617
+ errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
2618
+ }
2619
+ } else {
2620
+ errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
2621
+ }
2203
2622
  }
2204
2623
  }
2205
- return { filled, errors: errors2, fields_attempted: Object.keys(fields).length };
2624
+ return { filled, errors: errors2, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
2206
2625
  }
2207
2626
  async function waitForText(page, text, opts) {
2208
2627
  const timeout = opts?.timeout ?? 1e4;
@@ -9057,6 +9476,237 @@ var init_gallery = __esm(() => {
9057
9476
  init_schema();
9058
9477
  });
9059
9478
 
9479
+ // src/db/recordings.ts
9480
+ import { randomUUID as randomUUID5 } from "crypto";
9481
+ function deserialize2(row) {
9482
+ return {
9483
+ ...row,
9484
+ project_id: row.project_id ?? undefined,
9485
+ start_url: row.start_url ?? undefined,
9486
+ steps: JSON.parse(row.steps)
9487
+ };
9488
+ }
9489
+ function createRecording(data) {
9490
+ const db = getDatabase();
9491
+ const id = randomUUID5();
9492
+ 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 ?? []));
9493
+ return getRecording(id);
9494
+ }
9495
+ function getRecording(id) {
9496
+ const db = getDatabase();
9497
+ const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
9498
+ if (!row)
9499
+ throw new RecordingNotFoundError(id);
9500
+ return deserialize2(row);
9501
+ }
9502
+ function listRecordings(projectId) {
9503
+ const db = getDatabase();
9504
+ 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();
9505
+ return rows.map(deserialize2);
9506
+ }
9507
+ function updateRecording(id, data) {
9508
+ const db = getDatabase();
9509
+ const fields = [];
9510
+ const values = [];
9511
+ if (data.name !== undefined) {
9512
+ fields.push("name = ?");
9513
+ values.push(data.name);
9514
+ }
9515
+ if (data.steps !== undefined) {
9516
+ fields.push("steps = ?");
9517
+ values.push(JSON.stringify(data.steps));
9518
+ }
9519
+ if (data.start_url !== undefined) {
9520
+ fields.push("start_url = ?");
9521
+ values.push(data.start_url ?? null);
9522
+ }
9523
+ if (fields.length === 0)
9524
+ return getRecording(id);
9525
+ values.push(id);
9526
+ db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
9527
+ return getRecording(id);
9528
+ }
9529
+ var init_recordings = __esm(() => {
9530
+ init_schema();
9531
+ init_types();
9532
+ });
9533
+
9534
+ // src/lib/recorder.ts
9535
+ var exports_recorder = {};
9536
+ __export(exports_recorder, {
9537
+ stopRecording: () => stopRecording,
9538
+ startRecording: () => startRecording,
9539
+ replayRecording: () => replayRecording,
9540
+ recordStep: () => recordStep,
9541
+ listRecordings: () => listRecordings,
9542
+ getRecording: () => getRecording,
9543
+ exportRecording: () => exportRecording,
9544
+ attachPageListeners: () => attachPageListeners
9545
+ });
9546
+ function startRecording(sessionId, name, startUrl) {
9547
+ const steps = [];
9548
+ const recording = createRecording({ name, start_url: startUrl, steps });
9549
+ activeRecordings.set(recording.id, {
9550
+ id: recording.id,
9551
+ steps,
9552
+ cleanup: () => {}
9553
+ });
9554
+ return recording;
9555
+ }
9556
+ function attachPageListeners(page, recordingId) {
9557
+ const active = activeRecordings.get(recordingId);
9558
+ if (!active)
9559
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9560
+ const onFrameNav = () => {
9561
+ active.steps.push({
9562
+ type: "navigate",
9563
+ url: page.url(),
9564
+ timestamp: Date.now()
9565
+ });
9566
+ };
9567
+ page.on("framenavigated", onFrameNav);
9568
+ const cleanup = () => {
9569
+ page.off("framenavigated", onFrameNav);
9570
+ };
9571
+ active.cleanup = cleanup;
9572
+ }
9573
+ function recordStep(recordingId, step) {
9574
+ const active = activeRecordings.get(recordingId);
9575
+ if (!active)
9576
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9577
+ active.steps.push({ ...step, timestamp: Date.now() });
9578
+ }
9579
+ function stopRecording(recordingId) {
9580
+ const active = activeRecordings.get(recordingId);
9581
+ if (!active)
9582
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
9583
+ active.cleanup();
9584
+ activeRecordings.delete(recordingId);
9585
+ return updateRecording(recordingId, { steps: active.steps });
9586
+ }
9587
+ async function replayRecording(recordingId, page) {
9588
+ const recording = getRecording(recordingId);
9589
+ const startTime = Date.now();
9590
+ let executed = 0;
9591
+ let failed = 0;
9592
+ const errors2 = [];
9593
+ for (const step of recording.steps) {
9594
+ try {
9595
+ switch (step.type) {
9596
+ case "navigate":
9597
+ if (step.url)
9598
+ await navigate(page, step.url);
9599
+ break;
9600
+ case "click":
9601
+ if (step.selector)
9602
+ await click(page, step.selector);
9603
+ break;
9604
+ case "type":
9605
+ if (step.selector && step.value)
9606
+ await type(page, step.selector, step.value);
9607
+ break;
9608
+ case "scroll":
9609
+ await scroll(page, "down");
9610
+ break;
9611
+ case "hover":
9612
+ if (step.selector) {
9613
+ const el = await page.$(step.selector);
9614
+ if (el)
9615
+ await el.hover();
9616
+ }
9617
+ break;
9618
+ case "evaluate":
9619
+ if (step.value)
9620
+ await page.evaluate(step.value);
9621
+ break;
9622
+ case "wait":
9623
+ if (step.selector) {
9624
+ await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
9625
+ }
9626
+ break;
9627
+ }
9628
+ executed++;
9629
+ } catch (err) {
9630
+ failed++;
9631
+ errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
9632
+ }
9633
+ await new Promise((r) => setTimeout(r, 100));
9634
+ }
9635
+ return {
9636
+ recording_id: recordingId,
9637
+ success: failed === 0,
9638
+ steps_executed: executed,
9639
+ steps_failed: failed,
9640
+ errors: errors2,
9641
+ duration_ms: Date.now() - startTime
9642
+ };
9643
+ }
9644
+ function exportRecording(recordingId, format = "json") {
9645
+ const recording = getRecording(recordingId);
9646
+ if (format === "json") {
9647
+ return JSON.stringify(recording, null, 2);
9648
+ }
9649
+ if (format === "playwright") {
9650
+ const lines2 = [
9651
+ `import { test, expect } from '@playwright/test';`,
9652
+ ``,
9653
+ `test('${recording.name}', async ({ page }) => {`
9654
+ ];
9655
+ for (const step of recording.steps) {
9656
+ switch (step.type) {
9657
+ case "navigate":
9658
+ lines2.push(` await page.goto('${step.url}');`);
9659
+ break;
9660
+ case "click":
9661
+ lines2.push(` await page.click('${step.selector}');`);
9662
+ break;
9663
+ case "type":
9664
+ lines2.push(` await page.type('${step.selector}', '${step.value}');`);
9665
+ break;
9666
+ case "scroll":
9667
+ lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
9668
+ break;
9669
+ case "evaluate":
9670
+ lines2.push(` await page.evaluate(${step.value});`);
9671
+ break;
9672
+ }
9673
+ }
9674
+ lines2.push(`});`);
9675
+ return lines2.join(`
9676
+ `);
9677
+ }
9678
+ const lines = [
9679
+ `const puppeteer = require('puppeteer');`,
9680
+ ``,
9681
+ `(async () => {`,
9682
+ ` const browser = await puppeteer.launch();`,
9683
+ ` const page = await browser.newPage();`
9684
+ ];
9685
+ for (const step of recording.steps) {
9686
+ switch (step.type) {
9687
+ case "navigate":
9688
+ lines.push(` await page.goto('${step.url}');`);
9689
+ break;
9690
+ case "click":
9691
+ lines.push(` await page.click('${step.selector}');`);
9692
+ break;
9693
+ case "type":
9694
+ lines.push(` await page.type('${step.selector}', '${step.value}');`);
9695
+ break;
9696
+ }
9697
+ }
9698
+ lines.push(` await browser.close();`, `})();`);
9699
+ return lines.join(`
9700
+ `);
9701
+ }
9702
+ var activeRecordings;
9703
+ var init_recorder = __esm(() => {
9704
+ init_recordings();
9705
+ init_actions();
9706
+ init_types();
9707
+ activeRecordings = new Map;
9708
+ });
9709
+
9060
9710
  // src/lib/profiles.ts
9061
9711
  var exports_profiles = {};
9062
9712
  __export(exports_profiles, {
@@ -9066,23 +9716,23 @@ __export(exports_profiles, {
9066
9716
  deleteProfile: () => deleteProfile,
9067
9717
  applyProfile: () => applyProfile
9068
9718
  });
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";
9719
+ import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
9720
+ import { join as join8 } from "path";
9721
+ import { homedir as homedir8 } from "os";
9072
9722
  function getProfilesDir() {
9073
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
9074
- const dir = join7(dataDir, "profiles");
9075
- mkdirSync7(dir, { recursive: true });
9723
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
9724
+ const dir = join8(dataDir, "profiles");
9725
+ mkdirSync8(dir, { recursive: true });
9076
9726
  return dir;
9077
9727
  }
9078
9728
  function getProfileDir2(name) {
9079
- return join7(getProfilesDir(), name);
9729
+ return join8(getProfilesDir(), name);
9080
9730
  }
9081
9731
  async function saveProfile(page, name) {
9082
9732
  const dir = getProfileDir2(name);
9083
- mkdirSync7(dir, { recursive: true });
9733
+ mkdirSync8(dir, { recursive: true });
9084
9734
  const cookies = await page.context().cookies();
9085
- writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
9735
+ writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
9086
9736
  let localStorage2 = {};
9087
9737
  try {
9088
9738
  localStorage2 = await page.evaluate(() => {
@@ -9094,11 +9744,11 @@ async function saveProfile(page, name) {
9094
9744
  return result;
9095
9745
  });
9096
9746
  } catch {}
9097
- writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
9747
+ writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
9098
9748
  const savedAt = new Date().toISOString();
9099
9749
  const url = page.url();
9100
9750
  const meta = { saved_at: savedAt, url };
9101
- writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
9751
+ writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
9102
9752
  return {
9103
9753
  name,
9104
9754
  saved_at: savedAt,
@@ -9109,17 +9759,17 @@ async function saveProfile(page, name) {
9109
9759
  }
9110
9760
  function loadProfile(name) {
9111
9761
  const dir = getProfileDir2(name);
9112
- if (!existsSync3(dir)) {
9762
+ if (!existsSync4(dir)) {
9113
9763
  throw new Error(`Profile not found: ${name}`);
9114
9764
  }
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")) : {};
9765
+ const cookiesPath = join8(dir, "cookies.json");
9766
+ const storagePath = join8(dir, "storage.json");
9767
+ const metaPath2 = join8(dir, "meta.json");
9768
+ const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
9769
+ const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
9120
9770
  let savedAt = new Date().toISOString();
9121
9771
  let url;
9122
- if (existsSync3(metaPath2)) {
9772
+ if (existsSync4(metaPath2)) {
9123
9773
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
9124
9774
  savedAt = meta.saved_at ?? savedAt;
9125
9775
  url = meta.url;
@@ -9147,33 +9797,33 @@ async function applyProfile(page, profileData) {
9147
9797
  }
9148
9798
  function listProfiles() {
9149
9799
  const dir = getProfilesDir();
9150
- if (!existsSync3(dir))
9800
+ if (!existsSync4(dir))
9151
9801
  return [];
9152
- const entries = readdirSync2(dir, { withFileTypes: true });
9802
+ const entries = readdirSync3(dir, { withFileTypes: true });
9153
9803
  const profiles = [];
9154
9804
  for (const entry of entries) {
9155
9805
  if (!entry.isDirectory())
9156
9806
  continue;
9157
9807
  const name = entry.name;
9158
- const profileDir = join7(dir, name);
9808
+ const profileDir = join8(dir, name);
9159
9809
  let savedAt = "";
9160
9810
  let url;
9161
9811
  let cookieCount = 0;
9162
9812
  let storageKeyCount = 0;
9163
9813
  try {
9164
- const metaPath2 = join7(profileDir, "meta.json");
9165
- if (existsSync3(metaPath2)) {
9814
+ const metaPath2 = join8(profileDir, "meta.json");
9815
+ if (existsSync4(metaPath2)) {
9166
9816
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
9167
9817
  savedAt = meta.saved_at ?? "";
9168
9818
  url = meta.url;
9169
9819
  }
9170
- const cookiesPath = join7(profileDir, "cookies.json");
9171
- if (existsSync3(cookiesPath)) {
9820
+ const cookiesPath = join8(profileDir, "cookies.json");
9821
+ if (existsSync4(cookiesPath)) {
9172
9822
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
9173
9823
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
9174
9824
  }
9175
- const storagePath = join7(profileDir, "storage.json");
9176
- if (existsSync3(storagePath)) {
9825
+ const storagePath = join8(profileDir, "storage.json");
9826
+ if (existsSync4(storagePath)) {
9177
9827
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
9178
9828
  storageKeyCount = Object.keys(storage).length;
9179
9829
  }
@@ -9190,7 +9840,7 @@ function listProfiles() {
9190
9840
  }
9191
9841
  function deleteProfile(name) {
9192
9842
  const dir = getProfileDir2(name);
9193
- if (!existsSync3(dir))
9843
+ if (!existsSync4(dir))
9194
9844
  return false;
9195
9845
  try {
9196
9846
  rmSync(dir, { recursive: true, force: true });
@@ -9201,6 +9851,97 @@ function deleteProfile(name) {
9201
9851
  }
9202
9852
  var init_profiles = () => {};
9203
9853
 
9854
+ // src/lib/sanitize.ts
9855
+ var exports_sanitize = {};
9856
+ __export(exports_sanitize, {
9857
+ sanitizeText: () => sanitizeText,
9858
+ sanitizeHTML: () => sanitizeHTML
9859
+ });
9860
+ function sanitizeText(text) {
9861
+ let stripped = 0;
9862
+ const warnings = [];
9863
+ let clean = text;
9864
+ for (const pattern of INJECTION_PATTERNS) {
9865
+ pattern.lastIndex = 0;
9866
+ const matches = clean.match(pattern);
9867
+ if (matches) {
9868
+ stripped += matches.length;
9869
+ warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
9870
+ pattern.lastIndex = 0;
9871
+ clean = clean.replace(pattern, "[STRIPPED]");
9872
+ }
9873
+ }
9874
+ return { text: clean, stripped, warnings };
9875
+ }
9876
+ function sanitizeHTML(html) {
9877
+ let stripped = 0;
9878
+ const warnings = [];
9879
+ let clean = html;
9880
+ const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
9881
+ if (commentMatches) {
9882
+ for (const comment of commentMatches) {
9883
+ if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
9884
+ stripped++;
9885
+ warnings.push(`Stripped HTML comment (${comment.length} chars)`);
9886
+ }
9887
+ }
9888
+ clean = clean.replace(/<!--[\s\S]*?-->/g, "");
9889
+ }
9890
+ const hiddenPatterns = [
9891
+ /style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
9892
+ /style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
9893
+ /style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
9894
+ /style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
9895
+ /style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
9896
+ ];
9897
+ for (const pattern of hiddenPatterns) {
9898
+ pattern.lastIndex = 0;
9899
+ const matches = clean.match(pattern);
9900
+ if (matches) {
9901
+ stripped += matches.length;
9902
+ warnings.push(`Stripped ${matches.length} hidden elements`);
9903
+ pattern.lastIndex = 0;
9904
+ clean = clean.replace(pattern, "");
9905
+ }
9906
+ }
9907
+ const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
9908
+ const ariaHidden = clean.match(ariaHiddenPattern);
9909
+ if (ariaHidden) {
9910
+ stripped += ariaHidden.length;
9911
+ warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
9912
+ ariaHiddenPattern.lastIndex = 0;
9913
+ clean = clean.replace(ariaHiddenPattern, "");
9914
+ }
9915
+ const textResult = sanitizeText(clean);
9916
+ return {
9917
+ text: textResult.text,
9918
+ stripped: stripped + textResult.stripped,
9919
+ warnings: [...warnings, ...textResult.warnings]
9920
+ };
9921
+ }
9922
+ var INJECTION_PATTERNS;
9923
+ var init_sanitize = __esm(() => {
9924
+ INJECTION_PATTERNS = [
9925
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
9926
+ /ignore\s+(all\s+)?prior\s+instructions/gi,
9927
+ /disregard\s+(all\s+)?previous/gi,
9928
+ /forget\s+(all\s+)?previous/gi,
9929
+ /you\s+are\s+now\s+/gi,
9930
+ /new\s+instructions?\s*:/gi,
9931
+ /system\s+prompt\s*:/gi,
9932
+ /\[INST\]/gi,
9933
+ /\[\/INST\]/gi,
9934
+ /<\|im_start\|>/gi,
9935
+ /<\|im_end\|>/gi,
9936
+ /<<SYS>>/gi,
9937
+ /<<\/SYS>>/gi,
9938
+ /IMPORTANT:\s*ignore/gi,
9939
+ /CRITICAL:\s*override/gi,
9940
+ /assistant:\s/gi,
9941
+ /human:\s/gi
9942
+ ];
9943
+ });
9944
+
9204
9945
  // src/lib/annotate.ts
9205
9946
  var exports_annotate = {};
9206
9947
  __export(exports_annotate, {
@@ -9261,26 +10002,846 @@ var init_annotate = __esm(() => {
9261
10002
  import_sharp3 = __toESM(require_lib(), 1);
9262
10003
  });
9263
10004
 
10005
+ // src/lib/env-detector.ts
10006
+ var exports_env_detector = {};
10007
+ __export(exports_env_detector, {
10008
+ detectEnvironment: () => detectEnvironment
10009
+ });
10010
+ async function detectEnvironment(page) {
10011
+ const url = page.url();
10012
+ const signals = [];
10013
+ let score = { local: 0, dev: 0, staging: 0, prod: 0 };
10014
+ try {
10015
+ const u = new URL(url);
10016
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "0.0.0.0" || u.hostname.endsWith(".local")) {
10017
+ score.local += 5;
10018
+ signals.push(`URL hostname: ${u.hostname} \u2192 local`);
10019
+ } else if (u.hostname.match(/^(dev|development)\./i) || u.port !== "") {
10020
+ score.dev += 4;
10021
+ signals.push(`URL pattern: ${u.hostname}:${u.port} \u2192 dev`);
10022
+ } else if (u.hostname.match(/^(staging|stg|stage|preprod|uat)\./i)) {
10023
+ score.staging += 4;
10024
+ signals.push(`URL pattern: ${u.hostname} \u2192 staging`);
10025
+ } else {
10026
+ score.prod += 2;
10027
+ signals.push(`URL looks production: ${u.hostname}`);
10028
+ }
10029
+ if (u.port && !["80", "443", ""].includes(u.port)) {
10030
+ score.dev += 2;
10031
+ signals.push(`Non-standard port: ${u.port}`);
10032
+ }
10033
+ if (u.protocol === "https:") {
10034
+ score.prod += 1;
10035
+ signals.push("HTTPS \u2192 likely prod");
10036
+ } else {
10037
+ score.dev += 2;
10038
+ signals.push("HTTP \u2192 likely dev/local");
10039
+ }
10040
+ } catch {}
10041
+ try {
10042
+ const pageSignals = await page.evaluate(() => {
10043
+ const s = [];
10044
+ const w = window;
10045
+ const envVars = ["__ENV__", "__NEXT_DATA__", "__NUXT__", "process"];
10046
+ for (const v of envVars) {
10047
+ if (w[v]) {
10048
+ const env2 = w[v]?.env?.NODE_ENV ?? w[v]?.runtimeConfig?.public?.env ?? w[v]?.props?.pageProps?.env;
10049
+ if (env2)
10050
+ s.push(`window.${v}: ${env2}`);
10051
+ }
10052
+ }
10053
+ if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
10054
+ const fiber = document.querySelector("[data-reactroot]") || document.getElementById("__next") || document.getElementById("root");
10055
+ if (fiber)
10056
+ s.push("React app detected");
10057
+ }
10058
+ const envMeta = document.querySelector('meta[name="environment"], meta[name="env"], meta[name="deploy-env"]');
10059
+ if (envMeta)
10060
+ s.push(`meta[environment]: ${envMeta.getAttribute("content")}`);
10061
+ const scripts = document.querySelectorAll("script[src]");
10062
+ let minified = 0, unminified = 0;
10063
+ scripts.forEach((s2) => {
10064
+ const src = s2.getAttribute("src") ?? "";
10065
+ if (src.includes(".min.") || src.match(/\.[a-f0-9]{8,}\./))
10066
+ minified++;
10067
+ else if (src.endsWith(".js") && !src.includes("chunk"))
10068
+ unminified++;
10069
+ });
10070
+ if (unminified > minified && unminified > 2)
10071
+ s.push(`Unminified scripts (${unminified}/${minified + unminified}) \u2192 likely dev`);
10072
+ else if (minified > 0)
10073
+ s.push(`Minified/hashed scripts (${minified}/${minified + unminified}) \u2192 likely prod`);
10074
+ if (document.querySelector("[data-testid]"))
10075
+ s.push("data-testid attributes present \u2192 dev/staging");
10076
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
10077
+ s.push("Service worker active \u2192 likely prod");
10078
+ }
10079
+ if (w.Sentry)
10080
+ s.push("Sentry SDK loaded \u2192 prod monitoring");
10081
+ if (w.__DATADOG_SYNTHETICS_INLINED_SCRIPT)
10082
+ s.push("Datadog loaded \u2192 prod monitoring");
10083
+ if (w.LogRocket)
10084
+ s.push("LogRocket loaded \u2192 prod monitoring");
10085
+ if (w._lr_loaded)
10086
+ s.push("LogRocket loaded \u2192 prod monitoring");
10087
+ if (w.gtag || w.ga)
10088
+ s.push("Google Analytics loaded \u2192 likely prod");
10089
+ if (w.posthog || w._ph)
10090
+ s.push("PostHog loaded \u2192 prod analytics");
10091
+ if (w.mixpanel)
10092
+ s.push("Mixpanel loaded \u2192 prod analytics");
10093
+ const robots = document.querySelector('meta[name="robots"]');
10094
+ if (robots) {
10095
+ const content = robots.getAttribute("content") ?? "";
10096
+ if (content.includes("noindex"))
10097
+ s.push(`robots: noindex \u2192 staging/dev`);
10098
+ }
10099
+ return s;
10100
+ });
10101
+ for (const signal of pageSignals) {
10102
+ signals.push(signal);
10103
+ if (signal.includes("development") || signal.includes("\u2192 dev") || signal.includes("\u2192 likely dev"))
10104
+ score.dev += 2;
10105
+ if (signal.includes("production") || signal.includes("\u2192 prod") || signal.includes("\u2192 likely prod"))
10106
+ score.prod += 2;
10107
+ if (signal.includes("staging") || signal.includes("\u2192 staging"))
10108
+ score.staging += 2;
10109
+ if (signal.includes("monitoring") || signal.includes("analytics"))
10110
+ score.prod += 1;
10111
+ if (signal.includes("noindex")) {
10112
+ score.staging += 2;
10113
+ score.dev += 1;
10114
+ }
10115
+ }
10116
+ } catch {}
10117
+ const entries = Object.entries(score);
10118
+ entries.sort((a, b) => b[1] - a[1]);
10119
+ const [env, topScore] = entries[0];
10120
+ const [, secondScore] = entries[1];
10121
+ const confidence = topScore >= 5 ? "high" : topScore > secondScore + 1 ? "medium" : "low";
10122
+ return { env, confidence, signals };
10123
+ }
10124
+
10125
+ // src/lib/deep-performance.ts
10126
+ var exports_deep_performance = {};
10127
+ __export(exports_deep_performance, {
10128
+ getDeepPerformance: () => getDeepPerformance
10129
+ });
10130
+ function categorizeThirdParty(domain) {
10131
+ for (const [pattern, category] of Object.entries(THIRD_PARTY_CATEGORIES)) {
10132
+ if (domain.includes(pattern))
10133
+ return category;
10134
+ }
10135
+ return "other";
10136
+ }
10137
+ async function getDeepPerformance(page) {
10138
+ return page.evaluate(() => {
10139
+ const perf = performance;
10140
+ const entries = perf.getEntriesByType("resource");
10141
+ const navEntry = perf.getEntriesByType("navigation")[0];
10142
+ const paintEntries = perf.getEntriesByType("paint");
10143
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint")?.startTime;
10144
+ const ttfb = navEntry?.responseStart;
10145
+ const web_vitals = { fcp, ttfb };
10146
+ try {
10147
+ const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
10148
+ if (lcpEntries.length > 0)
10149
+ web_vitals.lcp = lcpEntries[lcpEntries.length - 1].startTime;
10150
+ } catch {}
10151
+ const byType = {};
10152
+ let totalBytes = 0;
10153
+ const resourceList = [];
10154
+ const pageDomain = location.hostname;
10155
+ const thirdPartyMap = new Map;
10156
+ for (const entry of entries) {
10157
+ const size = entry.transferSize || entry.encodedBodySize || 0;
10158
+ totalBytes += size;
10159
+ let type2 = entry.initiatorType || "other";
10160
+ if (type2 === "xmlhttprequest" || type2 === "fetch")
10161
+ type2 = "xhr";
10162
+ if (type2 === "link" && entry.name.match(/\.css/))
10163
+ type2 = "css";
10164
+ if (type2 === "img" || entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp|avif|ico)/i))
10165
+ type2 = "image";
10166
+ if (type2 === "script" || entry.name.match(/\.js/))
10167
+ type2 = "script";
10168
+ if (entry.name.match(/\.(woff2?|ttf|otf|eot)/i))
10169
+ type2 = "font";
10170
+ if (!byType[type2])
10171
+ byType[type2] = { count: 0, size_bytes: 0 };
10172
+ byType[type2].count++;
10173
+ byType[type2].size_bytes += size;
10174
+ resourceList.push({ url: entry.name, size_bytes: size, type: type2 });
10175
+ try {
10176
+ const domain = new URL(entry.name).hostname;
10177
+ if (domain !== pageDomain && !domain.endsWith(`.${pageDomain}`)) {
10178
+ if (!thirdPartyMap.has(domain))
10179
+ thirdPartyMap.set(domain, { scripts: 0, total_bytes: 0 });
10180
+ const tp = thirdPartyMap.get(domain);
10181
+ tp.scripts++;
10182
+ tp.total_bytes += size;
10183
+ }
10184
+ } catch {}
10185
+ }
10186
+ resourceList.sort((a, b) => b.size_bytes - a.size_bytes);
10187
+ const largest = resourceList.slice(0, 10).map((r) => ({
10188
+ url: r.url.length > 120 ? r.url.slice(0, 117) + "..." : r.url,
10189
+ size_bytes: r.size_bytes,
10190
+ type: r.type
10191
+ }));
10192
+ const third_party = Array.from(thirdPartyMap.entries()).map(([domain, data]) => ({ domain, ...data, category: "" })).sort((a, b) => b.total_bytes - a.total_bytes).slice(0, 15);
10193
+ const allNodes = document.querySelectorAll("*");
10194
+ let maxDepth = 0;
10195
+ let textNodes = 0;
10196
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);
10197
+ let node = walker.currentNode;
10198
+ while (node) {
10199
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
10200
+ textNodes++;
10201
+ let depth = 0;
10202
+ let parent = node.parentNode;
10203
+ while (parent) {
10204
+ depth++;
10205
+ parent = parent.parentNode;
10206
+ }
10207
+ if (depth > maxDepth)
10208
+ maxDepth = depth;
10209
+ node = walker.nextNode();
10210
+ }
10211
+ const mem = performance.memory;
10212
+ const memory = {
10213
+ js_heap_used_mb: mem ? Math.round(mem.usedJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
10214
+ js_heap_total_mb: mem ? Math.round(mem.totalJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
10215
+ js_heap_limit_mb: mem ? Math.round(mem.jsHeapSizeLimit / 1024 / 1024 * 100) / 100 : 0
10216
+ };
10217
+ return {
10218
+ web_vitals,
10219
+ resources: { total_transfer_bytes: totalBytes, total_resources: entries.length, by_type: byType, largest },
10220
+ third_party,
10221
+ dom: { node_count: document.all.length, max_depth: maxDepth, element_count: allNodes.length, text_node_count: textNodes },
10222
+ main_thread: { long_tasks: 0, total_blocking_ms: 0 },
10223
+ memory
10224
+ };
10225
+ }).then((result) => {
10226
+ for (const tp of result.third_party) {
10227
+ tp.category = categorizeThirdParty(tp.domain);
10228
+ }
10229
+ return result;
10230
+ });
10231
+ }
10232
+ var THIRD_PARTY_CATEGORIES;
10233
+ var init_deep_performance = __esm(() => {
10234
+ THIRD_PARTY_CATEGORIES = {
10235
+ "google-analytics.com": "analytics",
10236
+ "googletagmanager.com": "analytics",
10237
+ gtag: "analytics",
10238
+ "facebook.net": "social",
10239
+ "connect.facebook": "social",
10240
+ "stripe.com": "payment",
10241
+ "js.stripe.com": "payment",
10242
+ "sentry.io": "monitoring",
10243
+ "sentry-cdn": "monitoring",
10244
+ "posthog.com": "analytics",
10245
+ "ph.": "analytics",
10246
+ "intercom.io": "chat",
10247
+ "crisp.chat": "chat",
10248
+ "hotjar.com": "analytics",
10249
+ "clarity.ms": "analytics",
10250
+ "cdn.jsdelivr.net": "cdn",
10251
+ "cdnjs.cloudflare.com": "cdn",
10252
+ "unpkg.com": "cdn",
10253
+ "fonts.googleapis.com": "fonts",
10254
+ "fonts.gstatic.com": "fonts"
10255
+ };
10256
+ });
10257
+
10258
+ // src/lib/workflows.ts
10259
+ var exports_workflows = {};
10260
+ __export(exports_workflows, {
10261
+ saveWorkflowFromRecording: () => saveWorkflowFromRecording,
10262
+ saveWorkflow: () => saveWorkflow,
10263
+ runWorkflow: () => runWorkflow,
10264
+ listWorkflows: () => listWorkflows,
10265
+ getWorkflowByName: () => getWorkflowByName,
10266
+ getWorkflow: () => getWorkflow,
10267
+ deleteWorkflow: () => deleteWorkflow
10268
+ });
10269
+ import { randomUUID as randomUUID10 } from "crypto";
10270
+ function saveWorkflow(data) {
10271
+ const db = getDatabase();
10272
+ const id = randomUUID10();
10273
+ db.prepare("INSERT OR REPLACE INTO workflows (id, name, description, steps, start_url) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.description ?? null, JSON.stringify(data.steps), data.startUrl ?? null);
10274
+ return getWorkflow(id);
10275
+ }
10276
+ function saveWorkflowFromRecording(recordingId, name, description) {
10277
+ const db = getDatabase();
10278
+ const rec = db.query("SELECT steps, start_url FROM recordings WHERE id = ?").get(recordingId);
10279
+ if (!rec)
10280
+ throw new Error(`Recording not found: ${recordingId}`);
10281
+ const steps = JSON.parse(rec.steps);
10282
+ return saveWorkflow({ name, description, steps, startUrl: rec.start_url ?? undefined });
10283
+ }
10284
+ function getWorkflow(id) {
10285
+ const db = getDatabase();
10286
+ const row = db.query("SELECT * FROM workflows WHERE id = ?").get(id);
10287
+ if (!row)
10288
+ return null;
10289
+ return { ...row, steps: JSON.parse(row.steps) };
10290
+ }
10291
+ function getWorkflowByName(name) {
10292
+ const db = getDatabase();
10293
+ const row = db.query("SELECT * FROM workflows WHERE name = ?").get(name);
10294
+ if (!row)
10295
+ return null;
10296
+ return { ...row, steps: JSON.parse(row.steps) };
10297
+ }
10298
+ function listWorkflows() {
10299
+ const db = getDatabase();
10300
+ return db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all().map((row) => ({ ...row, steps: JSON.parse(row.steps) }));
10301
+ }
10302
+ function deleteWorkflow(name) {
10303
+ const db = getDatabase();
10304
+ return db.prepare("DELETE FROM workflows WHERE name = ?").run(name).changes > 0;
10305
+ }
10306
+ function recordRun(id, healed) {
10307
+ const db = getDatabase();
10308
+ if (healed) {
10309
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), last_heal = datetime('now'), heal_count = heal_count + 1, run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
10310
+ } else {
10311
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
10312
+ }
10313
+ }
10314
+ async function runWorkflow(workflow, page) {
10315
+ const t0 = Date.now();
10316
+ let executed = 0;
10317
+ let failed = 0;
10318
+ let healed = 0;
10319
+ const healedDetails = [];
10320
+ const errors2 = [];
10321
+ const updatedSteps = [...workflow.steps];
10322
+ for (let i = 0;i < workflow.steps.length; i++) {
10323
+ const step = workflow.steps[i];
10324
+ try {
10325
+ switch (step.type) {
10326
+ case "navigate":
10327
+ if (step.url)
10328
+ await page.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
10329
+ break;
10330
+ case "click":
10331
+ if (step.selector) {
10332
+ try {
10333
+ await page.click(step.selector, { timeout: 5000 });
10334
+ } catch {
10335
+ const result = await healSelector(page, step.selector);
10336
+ if (result.found && result.locator) {
10337
+ await result.locator.click();
10338
+ healed++;
10339
+ const healedSelector = `[healed:${result.method}]${step.selector}`;
10340
+ healedDetails.push({ step: i, original: step.selector, healed_to: healedSelector, method: result.method });
10341
+ } else {
10342
+ throw new Error(`Click failed: ${step.selector} (self-healing exhausted)`);
10343
+ }
10344
+ }
10345
+ }
10346
+ break;
10347
+ case "type":
10348
+ if (step.selector && step.value) {
10349
+ try {
10350
+ await page.fill(step.selector, step.value);
10351
+ } catch {
10352
+ const result = await healSelector(page, step.selector);
10353
+ if (result.found && result.locator) {
10354
+ await result.locator.fill(step.value);
10355
+ healed++;
10356
+ healedDetails.push({ step: i, original: step.selector, healed_to: `[healed:${result.method}]`, method: result.method });
10357
+ } else {
10358
+ throw new Error(`Type failed: ${step.selector} (self-healing exhausted)`);
10359
+ }
10360
+ }
10361
+ }
10362
+ break;
10363
+ case "scroll":
10364
+ if (step.y)
10365
+ await page.mouse.wheel(0, step.y);
10366
+ break;
10367
+ case "hover":
10368
+ if (step.selector) {
10369
+ try {
10370
+ await page.hover(step.selector);
10371
+ } catch {}
10372
+ }
10373
+ break;
10374
+ case "select":
10375
+ if (step.selector && step.value) {
10376
+ try {
10377
+ await page.selectOption(step.selector, step.value);
10378
+ } catch {}
10379
+ }
10380
+ break;
10381
+ case "wait":
10382
+ if (step.selector) {
10383
+ try {
10384
+ await page.waitForSelector(step.selector, { timeout: 1e4 });
10385
+ } catch {}
10386
+ } else {
10387
+ await new Promise((r) => setTimeout(r, step.timestamp || 1000));
10388
+ }
10389
+ break;
10390
+ case "evaluate":
10391
+ if (step.value)
10392
+ await page.evaluate(step.value);
10393
+ break;
10394
+ default:
10395
+ break;
10396
+ }
10397
+ executed++;
10398
+ } catch (err) {
10399
+ failed++;
10400
+ errors2.push(`Step ${i} (${step.type}): ${err instanceof Error ? err.message : String(err)}`);
10401
+ }
10402
+ }
10403
+ recordRun(workflow.id, healed > 0);
10404
+ return {
10405
+ success: failed === 0,
10406
+ steps_executed: executed,
10407
+ steps_failed: failed,
10408
+ steps_healed: healed,
10409
+ healed_details: healedDetails,
10410
+ errors: errors2,
10411
+ duration_ms: Date.now() - t0
10412
+ };
10413
+ }
10414
+ var init_workflows = __esm(() => {
10415
+ init_schema();
10416
+ });
10417
+
10418
+ // src/lib/auth-flow.ts
10419
+ var exports_auth_flow = {};
10420
+ __export(exports_auth_flow, {
10421
+ tryReplayAuth: () => tryReplayAuth,
10422
+ touchAuthFlow: () => touchAuthFlow,
10423
+ saveAuthFlow: () => saveAuthFlow,
10424
+ listAuthFlows: () => listAuthFlows,
10425
+ isAuthRedirect: () => isAuthRedirect,
10426
+ isAuthPage: () => isAuthPage,
10427
+ getAuthFlowByName: () => getAuthFlowByName,
10428
+ getAuthFlowByDomain: () => getAuthFlowByDomain,
10429
+ getAuthFlow: () => getAuthFlow,
10430
+ deleteAuthFlow: () => deleteAuthFlow
10431
+ });
10432
+ import { randomUUID as randomUUID11 } from "crypto";
10433
+ function saveAuthFlow(data) {
10434
+ const db = getDatabase();
10435
+ const id = randomUUID11();
10436
+ 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);
10437
+ return getAuthFlow(id);
10438
+ }
10439
+ function getAuthFlow(id) {
10440
+ const db = getDatabase();
10441
+ return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
10442
+ }
10443
+ function getAuthFlowByName(name) {
10444
+ const db = getDatabase();
10445
+ return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
10446
+ }
10447
+ function getAuthFlowByDomain(domain) {
10448
+ const db = getDatabase();
10449
+ return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
10450
+ }
10451
+ function listAuthFlows() {
10452
+ const db = getDatabase();
10453
+ return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
10454
+ }
10455
+ function deleteAuthFlow(name) {
10456
+ const db = getDatabase();
10457
+ const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
10458
+ return result.changes > 0;
10459
+ }
10460
+ function touchAuthFlow(id) {
10461
+ const db = getDatabase();
10462
+ db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
10463
+ }
10464
+ function isAuthPage(url) {
10465
+ return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
10466
+ }
10467
+ function isAuthRedirect(fromUrl, toUrl) {
10468
+ if (fromUrl === toUrl)
10469
+ return false;
10470
+ return isAuthPage(toUrl) && !isAuthPage(fromUrl);
10471
+ }
10472
+ async function tryReplayAuth(page, domain) {
10473
+ const flow = getAuthFlowByDomain(domain);
10474
+ if (!flow)
10475
+ return { replayed: false };
10476
+ if (flow.storage_state_path) {
10477
+ try {
10478
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
10479
+ if (existsSync5(flow.storage_state_path)) {
10480
+ const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
10481
+ if (state.cookies?.length) {
10482
+ await page.context().addCookies(state.cookies);
10483
+ await page.reload();
10484
+ await new Promise((r) => setTimeout(r, 1000));
10485
+ if (!isAuthPage(page.url())) {
10486
+ touchAuthFlow(flow.id);
10487
+ return { replayed: true, flow, method: "storage_state" };
10488
+ }
10489
+ }
10490
+ }
10491
+ } catch {}
10492
+ }
10493
+ if (flow.recording_id) {
10494
+ try {
10495
+ const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
10496
+ const result = await replayRecording2(flow.recording_id, page);
10497
+ if (result.success) {
10498
+ try {
10499
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
10500
+ const path = await saveStateFromPage2(page, flow.name);
10501
+ const db = getDatabase();
10502
+ db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
10503
+ } catch {}
10504
+ touchAuthFlow(flow.id);
10505
+ return { replayed: true, flow, method: "recording_replay" };
10506
+ }
10507
+ } catch {}
10508
+ }
10509
+ return { replayed: false, flow };
10510
+ }
10511
+ var AUTH_URL_PATTERNS;
10512
+ var init_auth_flow = __esm(() => {
10513
+ init_schema();
10514
+ AUTH_URL_PATTERNS = [
10515
+ /\/login/i,
10516
+ /\/signin/i,
10517
+ /\/sign-in/i,
10518
+ /\/auth/i,
10519
+ /\/sso/i,
10520
+ /\/oauth/i,
10521
+ /\/cas\/login/i,
10522
+ /accounts\.google\.com/i,
10523
+ /login\.microsoftonline\.com/i,
10524
+ /github\.com\/login/i,
10525
+ /auth0\.com/i
10526
+ ];
10527
+ });
10528
+
10529
+ // src/lib/vision-fallback.ts
10530
+ var exports_vision_fallback = {};
10531
+ __export(exports_vision_fallback, {
10532
+ findElementByVision: () => findElementByVision,
10533
+ clickByVision: () => clickByVision
10534
+ });
10535
+ async function findElementByVision(page, description, opts) {
10536
+ const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
10537
+ const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
10538
+ const base64 = screenshot.toString("base64");
10539
+ const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
10540
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
10541
+ if (!apiKey) {
10542
+ return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
10543
+ }
10544
+ try {
10545
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
10546
+ method: "POST",
10547
+ headers: {
10548
+ "content-type": "application/json",
10549
+ "x-api-key": apiKey,
10550
+ "anthropic-version": "2023-06-01"
10551
+ },
10552
+ body: JSON.stringify({
10553
+ model,
10554
+ max_tokens: 256,
10555
+ messages: [{
10556
+ role: "user",
10557
+ content: [
10558
+ {
10559
+ type: "image",
10560
+ source: { type: "base64", media_type: "image/jpeg", data: base64 }
10561
+ },
10562
+ {
10563
+ type: "text",
10564
+ text: `Find the element matching this description: "${description}"
10565
+
10566
+ The screenshot is ${viewport.width}x${viewport.height} pixels.
10567
+
10568
+ Reply with ONLY a JSON object (no markdown, no explanation):
10569
+ {"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
10570
+
10571
+ If you cannot find the element:
10572
+ {"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
10573
+ }
10574
+ ]
10575
+ }]
10576
+ })
10577
+ });
10578
+ const data = await response.json();
10579
+ const text = data.content?.[0]?.text ?? "";
10580
+ const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
10581
+ const result = JSON.parse(jsonStr);
10582
+ result.model = model;
10583
+ return result;
10584
+ } catch (err) {
10585
+ return {
10586
+ found: false,
10587
+ x: 0,
10588
+ y: 0,
10589
+ confidence: "none",
10590
+ description,
10591
+ model,
10592
+ error: err instanceof Error ? err.message : String(err)
10593
+ };
10594
+ }
10595
+ }
10596
+ async function clickByVision(page, description, opts) {
10597
+ const result = await findElementByVision(page, description, opts);
10598
+ if (result.found && result.x > 0 && result.y > 0) {
10599
+ await page.mouse.click(result.x, result.y);
10600
+ }
10601
+ return result;
10602
+ }
10603
+ var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
10604
+
10605
+ // src/lib/api-detector.ts
10606
+ var exports_api_detector = {};
10607
+ __export(exports_api_detector, {
10608
+ listDiscoveredAPIs: () => listDiscoveredAPIs,
10609
+ detectAPIs: () => detectAPIs
10610
+ });
10611
+ import { randomUUID as randomUUID12 } from "crypto";
10612
+ function detectAPIs(sessionId) {
10613
+ const db = getDatabase();
10614
+ const requests = db.query(`SELECT method, url, status_code, response_headers, body_size
10615
+ FROM network_log
10616
+ WHERE session_id = ?
10617
+ AND (response_headers LIKE '%application/json%' OR response_headers LIKE '%text/json%')
10618
+ AND status_code >= 200 AND status_code < 400
10619
+ ORDER BY timestamp DESC`).all(sessionId);
10620
+ const seen = new Map;
10621
+ for (const req of requests) {
10622
+ try {
10623
+ const urlObj = new URL(req.url);
10624
+ if (urlObj.pathname.match(/\.(js|css|png|jpg|svg|woff|ico)$/))
10625
+ continue;
10626
+ if (urlObj.hostname.includes("googleapis.com/identitytoolkit"))
10627
+ continue;
10628
+ if (urlObj.hostname.includes("posthog"))
10629
+ continue;
10630
+ if (urlObj.hostname.includes("sentry"))
10631
+ continue;
10632
+ const key = `${req.method} ${urlObj.origin}${urlObj.pathname}`;
10633
+ if (!seen.has(key)) {
10634
+ seen.set(key, {
10635
+ url: `${urlObj.origin}${urlObj.pathname}`,
10636
+ method: req.method,
10637
+ status_code: req.status_code,
10638
+ content_type: "application/json",
10639
+ response_schema: {},
10640
+ sample_size: req.body_size ?? 0
10641
+ });
10642
+ }
10643
+ } catch {}
10644
+ }
10645
+ const apis = Array.from(seen.values());
10646
+ for (const api of apis) {
10647
+ const id = randomUUID12();
10648
+ db.prepare("INSERT OR IGNORE INTO api_endpoints (id, session_id, url, method, status_code, content_type) VALUES (?, ?, ?, ?, ?, ?)").run(id, sessionId, api.url, api.method, api.status_code, api.content_type);
10649
+ }
10650
+ return apis;
10651
+ }
10652
+ function listDiscoveredAPIs(sessionId) {
10653
+ const db = getDatabase();
10654
+ if (sessionId) {
10655
+ return db.query("SELECT * FROM api_endpoints WHERE session_id = ? ORDER BY discovered_at DESC").all(sessionId);
10656
+ }
10657
+ return db.query("SELECT * FROM api_endpoints ORDER BY discovered_at DESC LIMIT 100").all();
10658
+ }
10659
+ var init_api_detector = __esm(() => {
10660
+ init_schema();
10661
+ });
10662
+
10663
+ // src/lib/structured-extract.ts
10664
+ var exports_structured_extract = {};
10665
+ __export(exports_structured_extract, {
10666
+ extractStructuredData: () => extractStructuredData
10667
+ });
10668
+ async function extractStructuredData(page) {
10669
+ return page.evaluate(() => {
10670
+ const result = { tables: [], lists: [], jsonLd: [], openGraph: {}, metaTags: {}, repeatedElements: [] };
10671
+ document.querySelectorAll("table").forEach((table, idx) => {
10672
+ const headers = [];
10673
+ table.querySelectorAll("thead th, thead td, tr:first-child th").forEach((th) => {
10674
+ headers.push(th.textContent?.trim() ?? "");
10675
+ });
10676
+ const rows = [];
10677
+ table.querySelectorAll("tbody tr, tr:not(:first-child)").forEach((tr) => {
10678
+ const row = [];
10679
+ tr.querySelectorAll("td, th").forEach((td) => {
10680
+ row.push(td.textContent?.trim() ?? "");
10681
+ });
10682
+ if (row.length > 0 && row.some((c) => c !== ""))
10683
+ rows.push(row);
10684
+ });
10685
+ if (rows.length > 0) {
10686
+ result.tables.push({ headers, rows, selector: `table:nth-of-type(${idx + 1})` });
10687
+ }
10688
+ });
10689
+ document.querySelectorAll("ul, ol").forEach((list, idx) => {
10690
+ const items = [];
10691
+ list.querySelectorAll(":scope > li").forEach((li) => {
10692
+ const text = li.textContent?.trim() ?? "";
10693
+ if (text)
10694
+ items.push(text);
10695
+ });
10696
+ if (items.length >= 3) {
10697
+ const tag = list.tagName.toLowerCase();
10698
+ result.lists.push({ items, selector: `${tag}:nth-of-type(${idx + 1})` });
10699
+ }
10700
+ });
10701
+ document.querySelectorAll('script[type="application/ld+json"]').forEach((script) => {
10702
+ try {
10703
+ result.jsonLd.push(JSON.parse(script.textContent ?? ""));
10704
+ } catch {}
10705
+ });
10706
+ document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
10707
+ const prop = meta.getAttribute("property")?.replace("og:", "") ?? "";
10708
+ result.openGraph[prop] = meta.getAttribute("content") ?? "";
10709
+ });
10710
+ document.querySelectorAll("meta[name]").forEach((meta) => {
10711
+ const name = meta.getAttribute("name") ?? "";
10712
+ if (name)
10713
+ result.metaTags[name] = meta.getAttribute("content") ?? "";
10714
+ });
10715
+ const classCounts = new Map;
10716
+ document.querySelectorAll("[class]").forEach((el) => {
10717
+ const cls = el.className.toString().trim();
10718
+ if (cls && cls.length > 5 && cls.length < 100) {
10719
+ if (!classCounts.has(cls))
10720
+ classCounts.set(cls, []);
10721
+ classCounts.get(cls).push(el);
10722
+ }
10723
+ });
10724
+ for (const [cls, elements] of classCounts) {
10725
+ if (elements.length >= 3 && elements.length <= 200) {
10726
+ const sample = elements.slice(0, 3).map((el) => el.textContent?.trim().slice(0, 100) ?? "");
10727
+ if (sample.some((s) => s.length > 10)) {
10728
+ result.repeatedElements.push({
10729
+ selector: `.${cls.split(" ")[0]}`,
10730
+ count: elements.length,
10731
+ sample
10732
+ });
10733
+ }
10734
+ }
10735
+ }
10736
+ result.repeatedElements.sort((a, b) => b.count - a.count);
10737
+ result.repeatedElements = result.repeatedElements.slice(0, 10);
10738
+ return result;
10739
+ });
10740
+ }
10741
+
10742
+ // src/lib/datasets.ts
10743
+ var exports_datasets = {};
10744
+ __export(exports_datasets, {
10745
+ saveDataset: () => saveDataset,
10746
+ listDatasets: () => listDatasets,
10747
+ getDatasetByName: () => getDatasetByName,
10748
+ getDataset: () => getDataset,
10749
+ exportDataset: () => exportDataset,
10750
+ deleteDataset: () => deleteDataset
10751
+ });
10752
+ import { randomUUID as randomUUID13 } from "crypto";
10753
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
10754
+ import { join as join9 } from "path";
10755
+ import { homedir as homedir9 } from "os";
10756
+ function saveDataset(data) {
10757
+ const db = getDatabase();
10758
+ const id = randomUUID13();
10759
+ const existing = db.query("SELECT id FROM datasets WHERE name = ?").get(data.name);
10760
+ if (existing) {
10761
+ db.prepare("UPDATE datasets SET data = ?, row_count = ?, source_url = ?, schema = ?, last_refresh = datetime('now'), updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(data.rows), data.rows.length, data.sourceUrl ?? null, data.schema ? JSON.stringify(data.schema) : null, data.name);
10762
+ return getDataset(existing.id);
10763
+ }
10764
+ db.prepare("INSERT INTO datasets (id, name, source_url, source_type, data, row_count, schema) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.name, data.sourceUrl ?? null, data.sourceType ?? "page", JSON.stringify(data.rows), data.rows.length, data.schema ? JSON.stringify(data.schema) : null);
10765
+ return getDataset(id);
10766
+ }
10767
+ function getDataset(id) {
10768
+ const db = getDatabase();
10769
+ const row = db.query("SELECT * FROM datasets WHERE id = ?").get(id);
10770
+ if (!row)
10771
+ return null;
10772
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
10773
+ }
10774
+ function getDatasetByName(name) {
10775
+ const db = getDatabase();
10776
+ const row = db.query("SELECT * FROM datasets WHERE name = ?").get(name);
10777
+ if (!row)
10778
+ return null;
10779
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
10780
+ }
10781
+ function listDatasets() {
10782
+ const db = getDatabase();
10783
+ return db.query("SELECT id, name, source_url, source_type, row_count, last_refresh, created_at, updated_at FROM datasets ORDER BY updated_at DESC").all().map((row) => ({ ...row, data: `${row.row_count} rows`, schema: null }));
10784
+ }
10785
+ function deleteDataset(name) {
10786
+ const db = getDatabase();
10787
+ return db.prepare("DELETE FROM datasets WHERE name = ?").run(name).changes > 0;
10788
+ }
10789
+ function exportDataset(name, format) {
10790
+ const dataset = getDatasetByName(name);
10791
+ if (!dataset)
10792
+ throw new Error(`Dataset '${name}' not found`);
10793
+ const dir = join9(process.env["BROWSER_DATA_DIR"] ?? join9(homedir9(), ".browser"), "exports");
10794
+ mkdirSync9(dir, { recursive: true });
10795
+ const filename = `${name}.${format}`;
10796
+ const path = join9(dir, filename);
10797
+ if (format === "csv") {
10798
+ const rows = dataset.data;
10799
+ if (rows.length === 0) {
10800
+ writeFileSync3(path, "");
10801
+ return { path, size: 0 };
10802
+ }
10803
+ const headers = Object.keys(rows[0]);
10804
+ const csvLines = [headers.join(",")];
10805
+ for (const row of rows) {
10806
+ csvLines.push(headers.map((h) => {
10807
+ const val = String(row[h] ?? "");
10808
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
10809
+ }).join(","));
10810
+ }
10811
+ const content = csvLines.join(`
10812
+ `);
10813
+ writeFileSync3(path, content);
10814
+ return { path, size: content.length };
10815
+ } else {
10816
+ const content = JSON.stringify(dataset.data, null, 2);
10817
+ writeFileSync3(path, content);
10818
+ return { path, size: content.length };
10819
+ }
10820
+ }
10821
+ var init_datasets = __esm(() => {
10822
+ init_schema();
10823
+ });
10824
+
9264
10825
  // src/lib/auth.ts
9265
10826
  var exports_auth = {};
9266
10827
  __export(exports_auth, {
9267
10828
  loginWithCredentials: () => loginWithCredentials,
9268
10829
  getCredentials: () => getCredentials
9269
10830
  });
9270
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
9271
- import { join as join8 } from "path";
9272
- import { homedir as homedir8 } from "os";
10831
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
10832
+ import { join as join10 } from "path";
10833
+ import { homedir as homedir10 } from "os";
9273
10834
  async function getCredentials(service) {
9274
10835
  try {
9275
- const { getSecret } = await import(`${homedir8()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
10836
+ const { getSecret } = await import(`${homedir10()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
9276
10837
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
9277
10838
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
9278
10839
  if (email?.value && password?.value) {
9279
10840
  return { email: email.value, password: password.value };
9280
10841
  }
9281
10842
  } catch {}
9282
- const secretsPath = join8(homedir8(), ".secrets");
9283
- if (existsSync4(secretsPath)) {
10843
+ const secretsPath = join10(homedir10(), ".secrets");
10844
+ if (existsSync5(secretsPath)) {
9284
10845
  const content = readFileSync3(secretsPath, "utf8");
9285
10846
  const lines = content.split(`
9286
10847
  `);
@@ -9457,14 +11018,14 @@ __export(exports_dist, {
9457
11018
  InvalidScopeError: () => InvalidScopeError,
9458
11019
  EntityNotFoundError: () => EntityNotFoundError,
9459
11020
  DuplicateMemoryError: () => DuplicateMemoryError,
9460
- DEFAULT_MODEL: () => DEFAULT_MODEL,
11021
+ DEFAULT_MODEL: () => DEFAULT_MODEL2,
9461
11022
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
9462
11023
  });
9463
11024
  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";
11025
+ import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
11026
+ import { dirname, join as join11, resolve } from "path";
11027
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
11028
+ import { homedir as homedir11 } from "os";
9468
11029
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
9469
11030
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
9470
11031
  import { homedir as homedir22 } from "os";
@@ -9478,8 +11039,8 @@ function isInMemoryDb(path) {
9478
11039
  function findNearestMementosDb(startDir) {
9479
11040
  let dir = resolve(startDir);
9480
11041
  while (true) {
9481
- const candidate = join9(dir, ".mementos", "mementos.db");
9482
- if (existsSync5(candidate))
11042
+ const candidate = join11(dir, ".mementos", "mementos.db");
11043
+ if (existsSync6(candidate))
9483
11044
  return candidate;
9484
11045
  const parent = dirname(dir);
9485
11046
  if (parent === dir)
@@ -9491,7 +11052,7 @@ function findNearestMementosDb(startDir) {
9491
11052
  function findGitRoot(startDir) {
9492
11053
  let dir = resolve(startDir);
9493
11054
  while (true) {
9494
- if (existsSync5(join9(dir, ".git")))
11055
+ if (existsSync6(join11(dir, ".git")))
9495
11056
  return dir;
9496
11057
  const parent = dirname(dir);
9497
11058
  if (parent === dir)
@@ -9511,25 +11072,25 @@ function getDbPath() {
9511
11072
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
9512
11073
  const gitRoot = findGitRoot(cwd);
9513
11074
  if (gitRoot) {
9514
- return join9(gitRoot, ".mementos", "mementos.db");
11075
+ return join11(gitRoot, ".mementos", "mementos.db");
9515
11076
  }
9516
11077
  }
9517
11078
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
9518
- return join9(home, ".mementos", "mementos.db");
11079
+ return join11(home, ".mementos", "mementos.db");
9519
11080
  }
9520
- function ensureDir(filePath) {
11081
+ function ensureDir2(filePath) {
9521
11082
  if (isInMemoryDb(filePath))
9522
11083
  return;
9523
11084
  const dir = dirname(resolve(filePath));
9524
- if (!existsSync5(dir)) {
9525
- mkdirSync8(dir, { recursive: true });
11085
+ if (!existsSync6(dir)) {
11086
+ mkdirSync10(dir, { recursive: true });
9526
11087
  }
9527
11088
  }
9528
11089
  function getDatabase2(dbPath) {
9529
11090
  if (_db2)
9530
11091
  return _db2;
9531
11092
  const path = dbPath || getDbPath();
9532
- ensureDir(path);
11093
+ ensureDir2(path);
9533
11094
  _db2 = new Database2(path, { create: true });
9534
11095
  _db2.run("PRAGMA journal_mode = WAL");
9535
11096
  _db2.run("PRAGMA busy_timeout = 5000");
@@ -11372,7 +12933,7 @@ function isValidCategory(value) {
11372
12933
  return VALID_CATEGORIES.includes(value);
11373
12934
  }
11374
12935
  function loadConfig() {
11375
- const configPath = join22(homedir9(), ".mementos", "config.json");
12936
+ const configPath = join22(homedir11(), ".mementos", "config.json");
11376
12937
  let fileConfig = {};
11377
12938
  if (existsSync22(configPath)) {
11378
12939
  try {
@@ -11399,10 +12960,10 @@ function loadConfig() {
11399
12960
  return merged;
11400
12961
  }
11401
12962
  function profilesDir() {
11402
- return join22(homedir9(), ".mementos", "profiles");
12963
+ return join22(homedir11(), ".mementos", "profiles");
11403
12964
  }
11404
12965
  function globalConfigPath() {
11405
- return join22(homedir9(), ".mementos", "config.json");
12966
+ return join22(homedir11(), ".mementos", "config.json");
11406
12967
  }
11407
12968
  function readGlobalConfig() {
11408
12969
  const p = globalConfigPath();
@@ -11416,8 +12977,8 @@ function readGlobalConfig() {
11416
12977
  }
11417
12978
  function writeGlobalConfig(data) {
11418
12979
  const p = globalConfigPath();
11419
- ensureDir2(dirname2(p));
11420
- writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
12980
+ ensureDir22(dirname2(p));
12981
+ writeFileSync4(p, JSON.stringify(data, null, 2), "utf-8");
11421
12982
  }
11422
12983
  function getActiveProfile() {
11423
12984
  const envProfile = process.env["MEMENTOS_PROFILE"];
@@ -11439,18 +13000,18 @@ function listProfiles2() {
11439
13000
  const dir = profilesDir();
11440
13001
  if (!existsSync22(dir))
11441
13002
  return [];
11442
- return readdirSync3(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
13003
+ return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
11443
13004
  }
11444
13005
  function deleteProfile2(name) {
11445
13006
  const dbPath = join22(profilesDir(), `${name}.db`);
11446
13007
  if (!existsSync22(dbPath))
11447
13008
  return false;
11448
- unlinkSync2(dbPath);
13009
+ unlinkSync3(dbPath);
11449
13010
  if (getActiveProfile() === name)
11450
13011
  setActiveProfile(null);
11451
13012
  return true;
11452
13013
  }
11453
- function ensureDir2(dir) {
13014
+ function ensureDir22(dir) {
11454
13015
  if (!existsSync22(dir)) {
11455
13016
  mkdirSync22(dir, { recursive: true });
11456
13017
  }
@@ -12572,7 +14133,7 @@ function writeConfig(config) {
12572
14133
  }
12573
14134
  function getActiveModel() {
12574
14135
  const config = readConfig();
12575
- return config.activeModel ?? DEFAULT_MODEL;
14136
+ return config.activeModel ?? DEFAULT_MODEL2;
12576
14137
  }
12577
14138
  function setActiveModel(modelId) {
12578
14139
  const config = readConfig();
@@ -12646,7 +14207,7 @@ Return JSON with this exact shape:
12646
14207
  examples: finalExamples,
12647
14208
  count: finalExamples.length
12648
14209
  };
12649
- }, DEFAULT_MODEL = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
14210
+ }, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
12650
14211
  var init_dist = __esm(() => {
12651
14212
  __defProp2 = Object.defineProperty;
12652
14213
  exports_database = {};
@@ -14082,10 +15643,10 @@ __export(exports_dist2, {
14082
15643
  acquireLock: () => acquireLock2
14083
15644
  });
14084
15645
  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";
15646
+ import { mkdirSync as mkdirSync11 } from "fs";
15647
+ import { join as join12, dirname as dirname3 } from "path";
15648
+ import { homedir as homedir12 } from "os";
15649
+ import { randomUUID as randomUUID14 } from "crypto";
14089
15650
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
14090
15651
  import { join as join33 } from "path";
14091
15652
  import { homedir as homedir33 } from "os";
@@ -14093,19 +15654,19 @@ import { readFileSync as readFileSync5 } from "fs";
14093
15654
  import { join as join23 } from "path";
14094
15655
  import { homedir as homedir23 } from "os";
14095
15656
  import { randomUUID as randomUUID22 } from "crypto";
14096
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
15657
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync5, mkdirSync as mkdirSync33 } from "fs";
14097
15658
  import { join as join43, dirname as dirname22 } from "path";
14098
15659
  import { homedir as homedir42 } from "os";
14099
15660
  function getDbPath2() {
14100
15661
  if (process.env.CONVERSATIONS_DB_PATH)
14101
15662
  return process.env.CONVERSATIONS_DB_PATH;
14102
- return join10(homedir10(), ".conversations", "messages.db");
15663
+ return join12(homedir12(), ".conversations", "messages.db");
14103
15664
  }
14104
15665
  function getDb() {
14105
15666
  if (db)
14106
15667
  return db;
14107
15668
  const dbPath = getDbPath2();
14108
- mkdirSync9(dirname3(dbPath), { recursive: true });
15669
+ mkdirSync11(dirname3(dbPath), { recursive: true });
14109
15670
  db = new Database3(dbPath, { create: true });
14110
15671
  db.exec("PRAGMA journal_mode = WAL");
14111
15672
  db.exec("PRAGMA busy_timeout = 5000");
@@ -14465,7 +16026,7 @@ function guessMimeType(name) {
14465
16026
  function sendMessage(opts) {
14466
16027
  const db2 = getDb();
14467
16028
  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)}`);
16029
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID14().slice(0, 8)}`);
14469
16030
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
14470
16031
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
14471
16032
  const blocking = opts.blocking ? 1 : 0;
@@ -15293,7 +16854,7 @@ function getAutoName() {
15293
16854
  cachedAutoName = name;
15294
16855
  try {
15295
16856
  mkdirSync33(dirname22(AGENT_ID_FILE), { recursive: true });
15296
- writeFileSync4(AGENT_ID_FILE, name + `
16857
+ writeFileSync5(AGENT_ID_FILE, name + `
15297
16858
  `, "utf-8");
15298
16859
  } catch {}
15299
16860
  return name;
@@ -17248,7 +18809,7 @@ Your code should look like:
17248
18809
  }
17249
18810
  }
17250
18811
  }
17251
- function checkPropTypes(typeSpecs, values, location, componentName, element) {
18812
+ function checkPropTypes(typeSpecs, values, location2, componentName, element) {
17252
18813
  {
17253
18814
  var has = Function.call.bind(hasOwnProperty);
17254
18815
  for (var typeSpecName in typeSpecs) {
@@ -17256,23 +18817,23 @@ Your code should look like:
17256
18817
  var error$1 = undefined;
17257
18818
  try {
17258
18819
  if (typeof typeSpecs[typeSpecName] !== "function") {
17259
- var err = Error((componentName || "React class") + ": " + location + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
18820
+ var err = Error((componentName || "React class") + ": " + location2 + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
17260
18821
  err.name = "Invariant Violation";
17261
18822
  throw err;
17262
18823
  }
17263
- error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
18824
+ error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location2, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
17264
18825
  } catch (ex) {
17265
18826
  error$1 = ex;
17266
18827
  }
17267
18828
  if (error$1 && !(error$1 instanceof Error)) {
17268
18829
  setCurrentlyValidatingElement(element);
17269
- error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location, typeSpecName, typeof error$1);
18830
+ error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location2, typeSpecName, typeof error$1);
17270
18831
  setCurrentlyValidatingElement(null);
17271
18832
  }
17272
18833
  if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
17273
18834
  loggedTypeFailures[error$1.message] = true;
17274
18835
  setCurrentlyValidatingElement(element);
17275
- error("Failed %s type: %s", location, error$1.message);
18836
+ error("Failed %s type: %s", location2, error$1.message);
17276
18837
  setCurrentlyValidatingElement(null);
17277
18838
  }
17278
18839
  }
@@ -18507,11 +20068,11 @@ __export(exports_dist3, {
18507
20068
  AgentNotFoundError: () => AgentNotFoundError2
18508
20069
  });
18509
20070
  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";
20071
+ import { existsSync as existsSync7, mkdirSync as mkdirSync12 } from "fs";
20072
+ import { dirname as dirname5, join as join13, resolve as resolve3 } from "path";
18512
20073
  import { existsSync as existsSync33 } from "fs";
18513
20074
  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";
20075
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
18515
20076
  import { join as join24 } from "path";
18516
20077
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
18517
20078
  import { join as join44 } from "path";
@@ -18741,8 +20302,8 @@ function isInMemoryDb2(path) {
18741
20302
  function findNearestTodosDb(startDir) {
18742
20303
  let dir = resolve3(startDir);
18743
20304
  while (true) {
18744
- const candidate = join11(dir, ".todos", "todos.db");
18745
- if (existsSync6(candidate))
20305
+ const candidate = join13(dir, ".todos", "todos.db");
20306
+ if (existsSync7(candidate))
18746
20307
  return candidate;
18747
20308
  const parent = dirname5(dir);
18748
20309
  if (parent === dir)
@@ -18754,7 +20315,7 @@ function findNearestTodosDb(startDir) {
18754
20315
  function findGitRoot2(startDir) {
18755
20316
  let dir = resolve3(startDir);
18756
20317
  while (true) {
18757
- if (existsSync6(join11(dir, ".git")))
20318
+ if (existsSync7(join13(dir, ".git")))
18758
20319
  return dir;
18759
20320
  const parent = dirname5(dir);
18760
20321
  if (parent === dir)
@@ -18774,18 +20335,18 @@ function getDbPath3() {
18774
20335
  if (process.env["TODOS_DB_SCOPE"] === "project") {
18775
20336
  const gitRoot = findGitRoot2(cwd);
18776
20337
  if (gitRoot) {
18777
- return join11(gitRoot, ".todos", "todos.db");
20338
+ return join13(gitRoot, ".todos", "todos.db");
18778
20339
  }
18779
20340
  }
18780
20341
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
18781
- return join11(home, ".todos", "todos.db");
20342
+ return join13(home, ".todos", "todos.db");
18782
20343
  }
18783
20344
  function ensureDir3(filePath) {
18784
20345
  if (isInMemoryDb2(filePath))
18785
20346
  return;
18786
20347
  const dir = dirname5(resolve3(filePath));
18787
- if (!existsSync6(dir)) {
18788
- mkdirSync10(dir, { recursive: true });
20348
+ if (!existsSync7(dir)) {
20349
+ mkdirSync12(dir, { recursive: true });
18789
20350
  }
18790
20351
  }
18791
20352
  function getDatabase3(dbPath) {
@@ -19244,14 +20805,14 @@ function ensureProject2(name, path, db2) {
19244
20805
  }
19245
20806
  return createProject3({ name, path }, d);
19246
20807
  }
19247
- function ensureDir22(dir) {
20808
+ function ensureDir23(dir) {
19248
20809
  if (!existsSync23(dir))
19249
20810
  mkdirSync24(dir, { recursive: true });
19250
20811
  }
19251
20812
  function listJsonFiles(dir) {
19252
20813
  if (!existsSync23(dir))
19253
20814
  return [];
19254
- return readdirSync4(dir).filter((f) => f.endsWith(".json"));
20815
+ return readdirSync5(dir).filter((f) => f.endsWith(".json"));
19255
20816
  }
19256
20817
  function readJsonFile(path) {
19257
20818
  try {
@@ -19261,7 +20822,7 @@ function readJsonFile(path) {
19261
20822
  }
19262
20823
  }
19263
20824
  function writeJsonFile(path, data) {
19264
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
20825
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
19265
20826
  `);
19266
20827
  }
19267
20828
  function readHighWaterMark(dir) {
@@ -19272,7 +20833,7 @@ function readHighWaterMark(dir) {
19272
20833
  return isNaN(val) ? 1 : val;
19273
20834
  }
19274
20835
  function writeHighWaterMark(dir, value) {
19275
- writeFileSync5(join24(dir, ".highwatermark"), String(value));
20836
+ writeFileSync6(join24(dir, ".highwatermark"), String(value));
19276
20837
  }
19277
20838
  function getFileMtimeMs(path) {
19278
20839
  try {
@@ -22116,7 +23677,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
22116
23677
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
22117
23678
  const dir = getTaskListDir(taskListId);
22118
23679
  if (!existsSync43(dir))
22119
- ensureDir22(dir);
23680
+ ensureDir23(dir);
22120
23681
  const filter = {};
22121
23682
  if (projectId)
22122
23683
  filter["project_id"] = projectId;
@@ -22334,7 +23895,7 @@ function metadataKey(agent) {
22334
23895
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
22335
23896
  const dir = getTaskListDir2(agent, taskListId);
22336
23897
  if (!existsSync52(dir))
22337
- ensureDir22(dir);
23898
+ ensureDir23(dir);
22338
23899
  const filter = {};
22339
23900
  if (projectId)
22340
23901
  filter["project_id"] = projectId;
@@ -23862,9 +25423,9 @@ __export(exports_dist4, {
23862
25423
  CATEGORIES: () => CATEGORIES,
23863
25424
  AGENT_TARGETS: () => AGENT_TARGETS
23864
25425
  });
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";
25426
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
25427
+ import { join as join14, dirname as dirname6 } from "path";
25428
+ import { homedir as homedir13 } from "os";
23868
25429
  import { fileURLToPath } from "url";
23869
25430
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
23870
25431
  import { join as join25 } from "path";
@@ -23975,35 +25536,35 @@ function normalizeSkillName(name) {
23975
25536
  function findSkillsDir() {
23976
25537
  let dir = __dirname2;
23977
25538
  for (let i = 0;i < 5; i++) {
23978
- const candidate = join12(dir, "skills");
23979
- if (existsSync7(candidate)) {
25539
+ const candidate = join14(dir, "skills");
25540
+ if (existsSync8(candidate)) {
23980
25541
  return candidate;
23981
25542
  }
23982
25543
  dir = dirname6(dir);
23983
25544
  }
23984
- return join12(__dirname2, "..", "skills");
25545
+ return join14(__dirname2, "..", "skills");
23985
25546
  }
23986
25547
  function getSkillPath(name) {
23987
25548
  const skillName = normalizeSkillName(name);
23988
- return join12(SKILLS_DIR, skillName);
25549
+ return join14(SKILLS_DIR, skillName);
23989
25550
  }
23990
25551
  function skillExists(name) {
23991
- return existsSync7(getSkillPath(name));
25552
+ return existsSync8(getSkillPath(name));
23992
25553
  }
23993
25554
  function installSkill(name, options = {}) {
23994
25555
  const { targetDir = process.cwd(), overwrite = false } = options;
23995
25556
  const skillName = normalizeSkillName(name);
23996
25557
  const sourcePath = getSkillPath(name);
23997
- const destDir = join12(targetDir, ".skills");
23998
- const destPath = join12(destDir, skillName);
23999
- if (!existsSync7(sourcePath)) {
25558
+ const destDir = join14(targetDir, ".skills");
25559
+ const destPath = join14(destDir, skillName);
25560
+ if (!existsSync8(sourcePath)) {
24000
25561
  return {
24001
25562
  skill: name,
24002
25563
  success: false,
24003
25564
  error: `Skill '${name}' not found`
24004
25565
  };
24005
25566
  }
24006
- if (existsSync7(destPath) && !overwrite) {
25567
+ if (existsSync8(destPath) && !overwrite) {
24007
25568
  return {
24008
25569
  skill: name,
24009
25570
  success: false,
@@ -24012,10 +25573,10 @@ function installSkill(name, options = {}) {
24012
25573
  };
24013
25574
  }
24014
25575
  try {
24015
- if (!existsSync7(destDir)) {
24016
- mkdirSync11(destDir, { recursive: true });
25576
+ if (!existsSync8(destDir)) {
25577
+ mkdirSync13(destDir, { recursive: true });
24017
25578
  }
24018
- if (existsSync7(destPath) && overwrite) {
25579
+ if (existsSync8(destPath) && overwrite) {
24019
25580
  rmSync2(destPath, { recursive: true, force: true });
24020
25581
  }
24021
25582
  cpSync(sourcePath, destPath, {
@@ -24054,10 +25615,10 @@ function installSkills(names, options = {}) {
24054
25615
  return names.map((name) => installSkill(name, options));
24055
25616
  }
24056
25617
  function updateSkillsIndex(skillsDir) {
24057
- const indexPath = join12(skillsDir, "index.ts");
25618
+ const indexPath = join14(skillsDir, "index.ts");
24058
25619
  const meta = loadMeta(skillsDir);
24059
25620
  const disabledSet = new Set(meta.disabled || []);
24060
- const skills = readdirSync5(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
25621
+ const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
24061
25622
  const exports = skills.map((s) => {
24062
25623
  const name = s.replace("skill-", "").replace(/-/g, "_");
24063
25624
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -24070,14 +25631,14 @@ function updateSkillsIndex(skillsDir) {
24070
25631
 
24071
25632
  ${exports}
24072
25633
  `;
24073
- writeFileSync6(indexPath, content);
25634
+ writeFileSync7(indexPath, content);
24074
25635
  }
24075
25636
  function getMetaPath(skillsDir) {
24076
- return join12(skillsDir, ".meta.json");
25637
+ return join14(skillsDir, ".meta.json");
24077
25638
  }
24078
25639
  function loadMeta(skillsDir) {
24079
25640
  const metaPath2 = getMetaPath(skillsDir);
24080
- if (existsSync7(metaPath2)) {
25641
+ if (existsSync8(metaPath2)) {
24081
25642
  try {
24082
25643
  return JSON.parse(readFileSync7(metaPath2, "utf-8"));
24083
25644
  } catch {}
@@ -24085,15 +25646,15 @@ function loadMeta(skillsDir) {
24085
25646
  return { skills: {} };
24086
25647
  }
24087
25648
  function saveMeta(skillsDir, meta) {
24088
- writeFileSync6(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
25649
+ writeFileSync7(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
24089
25650
  }
24090
25651
  function recordInstall(skillsDir, name) {
24091
25652
  const meta = loadMeta(skillsDir);
24092
25653
  const skillName = normalizeSkillName(name);
24093
25654
  let version = "unknown";
24094
25655
  try {
24095
- const pkgPath = join12(skillsDir, skillName, "package.json");
24096
- if (existsSync7(pkgPath)) {
25656
+ const pkgPath = join14(skillsDir, skillName, "package.json");
25657
+ if (existsSync8(pkgPath)) {
24097
25658
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
24098
25659
  version = pkg.version || "unknown";
24099
25660
  }
@@ -24107,12 +25668,12 @@ function recordRemove(skillsDir, name) {
24107
25668
  saveMeta(skillsDir, meta);
24108
25669
  }
24109
25670
  function getInstallMeta(targetDir = process.cwd()) {
24110
- return loadMeta(join12(targetDir, ".skills"));
25671
+ return loadMeta(join14(targetDir, ".skills"));
24111
25672
  }
24112
25673
  function disableSkill(name, targetDir = process.cwd()) {
24113
- const skillsDir = join12(targetDir, ".skills");
25674
+ const skillsDir = join14(targetDir, ".skills");
24114
25675
  const skillName = normalizeSkillName(name);
24115
- if (!existsSync7(join12(skillsDir, skillName)))
25676
+ if (!existsSync8(join14(skillsDir, skillName)))
24116
25677
  return false;
24117
25678
  const meta = loadMeta(skillsDir);
24118
25679
  const disabled = new Set(meta.disabled || []);
@@ -24125,7 +25686,7 @@ function disableSkill(name, targetDir = process.cwd()) {
24125
25686
  return true;
24126
25687
  }
24127
25688
  function enableSkill(name, targetDir = process.cwd()) {
24128
- const skillsDir = join12(targetDir, ".skills");
25689
+ const skillsDir = join14(targetDir, ".skills");
24129
25690
  const meta = loadMeta(skillsDir);
24130
25691
  const disabled = new Set(meta.disabled || []);
24131
25692
  if (!disabled.has(name))
@@ -24137,24 +25698,24 @@ function enableSkill(name, targetDir = process.cwd()) {
24137
25698
  return true;
24138
25699
  }
24139
25700
  function getDisabledSkills(targetDir = process.cwd()) {
24140
- const meta = loadMeta(join12(targetDir, ".skills"));
25701
+ const meta = loadMeta(join14(targetDir, ".skills"));
24141
25702
  return meta.disabled || [];
24142
25703
  }
24143
25704
  function getInstalledSkills(targetDir = process.cwd()) {
24144
- const skillsDir = join12(targetDir, ".skills");
24145
- if (!existsSync7(skillsDir)) {
25705
+ const skillsDir = join14(targetDir, ".skills");
25706
+ if (!existsSync8(skillsDir)) {
24146
25707
  return [];
24147
25708
  }
24148
- return readdirSync5(skillsDir).filter((f) => {
24149
- const fullPath = join12(skillsDir, f);
25709
+ return readdirSync6(skillsDir).filter((f) => {
25710
+ const fullPath = join14(skillsDir, f);
24150
25711
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
24151
25712
  }).map((f) => f.replace("skill-", ""));
24152
25713
  }
24153
25714
  function removeSkill(name, targetDir = process.cwd()) {
24154
25715
  const skillName = normalizeSkillName(name);
24155
- const skillsDir = join12(targetDir, ".skills");
24156
- const skillPath = join12(skillsDir, skillName);
24157
- if (!existsSync7(skillPath)) {
25716
+ const skillsDir = join14(targetDir, ".skills");
25717
+ const skillPath = join14(skillsDir, skillName);
25718
+ if (!existsSync8(skillPath)) {
24158
25719
  return false;
24159
25720
  }
24160
25721
  rmSync2(skillPath, { recursive: true, force: true });
@@ -24165,24 +25726,24 @@ function removeSkill(name, targetDir = process.cwd()) {
24165
25726
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
24166
25727
  const agentDir = `.${agent}`;
24167
25728
  if (scope === "project") {
24168
- return join12(projectDir || process.cwd(), agentDir, "skills");
25729
+ return join14(projectDir || process.cwd(), agentDir, "skills");
24169
25730
  }
24170
- return join12(homedir11(), agentDir, "skills");
25731
+ return join14(homedir13(), agentDir, "skills");
24171
25732
  }
24172
25733
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
24173
25734
  const skillName = normalizeSkillName(name);
24174
- return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
25735
+ return join14(getAgentSkillsDir(agent, scope, projectDir), skillName);
24175
25736
  }
24176
25737
  function installSkillForAgent(name, options, generateSkillMd) {
24177
25738
  const { agent, scope = "global", projectDir } = options;
24178
25739
  const skillName = normalizeSkillName(name);
24179
25740
  const sourcePath = getSkillPath(name);
24180
- if (!existsSync7(sourcePath)) {
25741
+ if (!existsSync8(sourcePath)) {
24181
25742
  return { skill: name, success: false, error: `Skill '${name}' not found` };
24182
25743
  }
24183
25744
  let skillMdContent = null;
24184
- const skillMdPath = join12(sourcePath, "SKILL.md");
24185
- if (existsSync7(skillMdPath)) {
25745
+ const skillMdPath = join14(sourcePath, "SKILL.md");
25746
+ if (existsSync8(skillMdPath)) {
24186
25747
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
24187
25748
  } else if (generateSkillMd) {
24188
25749
  skillMdContent = generateSkillMd(name);
@@ -24192,8 +25753,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
24192
25753
  }
24193
25754
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
24194
25755
  if (scope === "global") {
24195
- const agentBaseDir2 = join12(homedir11(), `.${agent}`);
24196
- if (!existsSync7(agentBaseDir2)) {
25756
+ const agentBaseDir2 = join14(homedir13(), `.${agent}`);
25757
+ if (!existsSync8(agentBaseDir2)) {
24197
25758
  const agentLabels = {
24198
25759
  claude: "Claude Code",
24199
25760
  codex: "Codex CLI",
@@ -24216,8 +25777,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
24216
25777
  }
24217
25778
  }
24218
25779
  try {
24219
- mkdirSync11(destDir, { recursive: true });
24220
- writeFileSync6(join12(destDir, "SKILL.md"), skillMdContent);
25780
+ mkdirSync13(destDir, { recursive: true });
25781
+ writeFileSync7(join14(destDir, "SKILL.md"), skillMdContent);
24221
25782
  return { skill: name, success: true, path: destDir };
24222
25783
  } catch (error) {
24223
25784
  return {
@@ -24230,7 +25791,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
24230
25791
  function removeSkillForAgent(name, options) {
24231
25792
  const { agent, scope = "global", projectDir } = options;
24232
25793
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
24233
- if (!existsSync7(destDir)) {
25794
+ if (!existsSync8(destDir)) {
24234
25795
  return false;
24235
25796
  }
24236
25797
  rmSync2(destDir, { recursive: true, force: true });
@@ -26154,7 +27715,7 @@ __export(exports_cron_manager, {
26154
27715
  deleteCronJob: () => deleteCronJob,
26155
27716
  createCronJob: () => createCronJob
26156
27717
  });
26157
- import { randomUUID as randomUUID11 } from "crypto";
27718
+ import { randomUUID as randomUUID15 } from "crypto";
26158
27719
  function ensureCronTable() {
26159
27720
  const db2 = getDatabase();
26160
27721
  db2.exec(`
@@ -26183,7 +27744,7 @@ function ensureCronTable() {
26183
27744
  function createCronJob(schedule, task, name) {
26184
27745
  ensureCronTable();
26185
27746
  const db2 = getDatabase();
26186
- const id = randomUUID11();
27747
+ const id = randomUUID15();
26187
27748
  db2.prepare(`
26188
27749
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
26189
27750
  VALUES (?, ?, ?, ?, 1)
@@ -26241,7 +27802,7 @@ function getCronEvents(jobId, limit = 10) {
26241
27802
  async function executeCronJob(job) {
26242
27803
  ensureCronTable();
26243
27804
  const db2 = getDatabase();
26244
- const eventId = randomUUID11();
27805
+ const eventId = randomUUID15();
26245
27806
  const startedAt = new Date().toISOString();
26246
27807
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
26247
27808
  try {
@@ -26332,7 +27893,7 @@ __export(exports_url_watcher, {
26332
27893
  deleteWatchJob: () => deleteWatchJob,
26333
27894
  createWatchJob: () => createWatchJob
26334
27895
  });
26335
- import { randomUUID as randomUUID12 } from "crypto";
27896
+ import { randomUUID as randomUUID16 } from "crypto";
26336
27897
  import { createHash } from "crypto";
26337
27898
  function ensureWatchTables() {
26338
27899
  const db2 = getDatabase();
@@ -26364,7 +27925,7 @@ function ensureWatchTables() {
26364
27925
  function createWatchJob(url, schedule, opts) {
26365
27926
  ensureWatchTables();
26366
27927
  const db2 = getDatabase();
26367
- const id = randomUUID12();
27928
+ const id = randomUUID16();
26368
27929
  db2.prepare(`
26369
27930
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
26370
27931
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -26400,7 +27961,7 @@ function getWatchEvents(watchId, limit = 20) {
26400
27961
  async function checkWatchJob(job) {
26401
27962
  ensureWatchTables();
26402
27963
  const db2 = getDatabase();
26403
- const eventId = randomUUID12();
27964
+ const eventId = randomUUID16();
26404
27965
  const checkedAt = new Date().toISOString();
26405
27966
  let newContent = "";
26406
27967
  try {
@@ -30552,23 +32113,23 @@ var NEVER = INVALID;
30552
32113
  init_session();
30553
32114
  init_actions();
30554
32115
  import { readFileSync as readFileSync8 } from "fs";
30555
- import { join as join13 } from "path";
32116
+ import { join as join15 } from "path";
30556
32117
 
30557
32118
  // src/lib/screenshot.ts
30558
32119
  init_types();
30559
32120
  init_gallery();
30560
32121
  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";
32122
+ import { join as join4 } from "path";
32123
+ import { mkdirSync as mkdirSync4 } from "fs";
32124
+ import { homedir as homedir4 } from "os";
30564
32125
  function getDataDir2() {
30565
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
32126
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
30566
32127
  }
30567
32128
  function getScreenshotDir(projectId) {
30568
- const base = join3(getDataDir2(), "screenshots");
32129
+ const base = join4(getDataDir2(), "screenshots");
30569
32130
  const date = new Date().toISOString().split("T")[0];
30570
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
30571
- mkdirSync3(dir, { recursive: true });
32131
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
32132
+ mkdirSync4(dir, { recursive: true });
30572
32133
  return dir;
30573
32134
  }
30574
32135
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -30583,7 +32144,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
30583
32144
  }
30584
32145
  }
30585
32146
  async function generateThumbnail(raw, dir, stem) {
30586
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
32147
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
30587
32148
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
30588
32149
  await Bun.write(thumbPath, thumbBuffer);
30589
32150
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -30640,7 +32201,7 @@ async function takeScreenshot(page, opts) {
30640
32201
  const compressedSizeBytes = finalBuffer.length;
30641
32202
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
30642
32203
  const ext = format;
30643
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
32204
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
30644
32205
  await Bun.write(screenshotPath, finalBuffer);
30645
32206
  let thumbnailPath;
30646
32207
  let thumbnailBase64;
@@ -30700,12 +32261,12 @@ async function takeScreenshot(page, opts) {
30700
32261
  }
30701
32262
  async function generatePDF(page, opts) {
30702
32263
  try {
30703
- const base = join3(getDataDir2(), "pdfs");
32264
+ const base = join4(getDataDir2(), "pdfs");
30704
32265
  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 });
32266
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
32267
+ mkdirSync4(dir, { recursive: true });
30707
32268
  const timestamp = Date.now();
30708
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
32269
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
30709
32270
  const buffer = await page.pdf({
30710
32271
  path: pdfPath,
30711
32272
  format: opts?.format ?? "A4",
@@ -30726,118 +32287,8 @@ async function generatePDF(page, opts) {
30726
32287
  // src/mcp/index.ts
30727
32288
  init_network();
30728
32289
 
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
32290
  // src/lib/performance.ts
32291
+ init_cdp();
30841
32292
  async function getPerformanceMetrics(page) {
30842
32293
  const navTiming = await page.evaluate(() => {
30843
32294
  const t = performance.timing;
@@ -30929,144 +32380,8 @@ async function setSessionStorage(page, key, value) {
30929
32380
  await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
30930
32381
  }
30931
32382
 
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
- }
32383
+ // src/mcp/index.ts
32384
+ init_recorder();
31070
32385
 
31071
32386
  // src/lib/crawler.ts
31072
32387
  init_types();
@@ -31249,16 +32564,16 @@ init_gallery();
31249
32564
 
31250
32565
  // src/lib/downloads.ts
31251
32566
  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";
32567
+ import { join as join5, basename, extname } from "path";
32568
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
32569
+ import { homedir as homedir5 } from "os";
31255
32570
  function getDataDir3() {
31256
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
32571
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
31257
32572
  }
31258
32573
  function getDownloadsDir(sessionId) {
31259
- const base = join4(getDataDir3(), "downloads");
31260
- const dir = sessionId ? join4(base, sessionId) : base;
31261
- mkdirSync4(dir, { recursive: true });
32574
+ const base = join5(getDataDir3(), "downloads");
32575
+ const dir = sessionId ? join5(base, sessionId) : base;
32576
+ mkdirSync5(dir, { recursive: true });
31262
32577
  return dir;
31263
32578
  }
31264
32579
  function metaPath(filePath) {
@@ -31270,7 +32585,7 @@ function saveToDownloads(buffer, filename, opts) {
31270
32585
  const ext = extname(filename) || "";
31271
32586
  const stem = basename(filename, ext);
31272
32587
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
31273
- const filePath = join4(dir, uniqueName);
32588
+ const filePath = join5(dir, uniqueName);
31274
32589
  writeFileSync(filePath, buffer);
31275
32590
  const meta = {
31276
32591
  id,
@@ -31299,20 +32614,20 @@ function listDownloads(sessionId) {
31299
32614
  const dir = getDownloadsDir(sessionId);
31300
32615
  const results = [];
31301
32616
  function scanDir(d) {
31302
- if (!existsSync(d))
32617
+ if (!existsSync2(d))
31303
32618
  return;
31304
- const entries = readdirSync(d);
32619
+ const entries = readdirSync2(d);
31305
32620
  for (const entry of entries) {
31306
32621
  if (entry.endsWith(".meta.json"))
31307
32622
  continue;
31308
- const full = join4(d, entry);
32623
+ const full = join5(d, entry);
31309
32624
  const stat = statSync(full);
31310
32625
  if (stat.isDirectory()) {
31311
32626
  scanDir(full);
31312
32627
  continue;
31313
32628
  }
31314
32629
  const mpath = metaPath(full);
31315
- if (!existsSync(mpath))
32630
+ if (!existsSync2(mpath))
31316
32631
  continue;
31317
32632
  try {
31318
32633
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -31342,9 +32657,9 @@ function deleteDownload(id, sessionId) {
31342
32657
  if (!file)
31343
32658
  return false;
31344
32659
  try {
31345
- unlinkSync(file.path);
31346
- if (existsSync(file.meta_path))
31347
- unlinkSync(file.meta_path);
32660
+ unlinkSync2(file.path);
32661
+ if (existsSync2(file.meta_path))
32662
+ unlinkSync2(file.meta_path);
31348
32663
  return true;
31349
32664
  } catch {
31350
32665
  return false;
@@ -31390,9 +32705,9 @@ function detectType(filename) {
31390
32705
 
31391
32706
  // src/lib/gallery-diff.ts
31392
32707
  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";
32708
+ import { join as join6 } from "path";
32709
+ import { mkdirSync as mkdirSync6 } from "fs";
32710
+ import { homedir as homedir6 } from "os";
31396
32711
  async function diffImages(path1, path2) {
31397
32712
  const img1 = import_sharp2.default(path1);
31398
32713
  const img2 = import_sharp2.default(path2);
@@ -31423,10 +32738,10 @@ async function diffImages(path1, path2) {
31423
32738
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
31424
32739
  }
31425
32740
  }
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`);
32741
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
32742
+ const diffDir = join6(dataDir, "diffs");
32743
+ mkdirSync6(diffDir, { recursive: true });
32744
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
31430
32745
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
31431
32746
  await Bun.write(diffPath, diffImageBuffer);
31432
32747
  return {
@@ -31442,9 +32757,9 @@ async function diffImages(path1, path2) {
31442
32757
  init_snapshot();
31443
32758
 
31444
32759
  // 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";
32760
+ import { join as join7 } from "path";
32761
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
32762
+ import { homedir as homedir7 } from "os";
31448
32763
  async function persistFile(localPath, opts) {
31449
32764
  try {
31450
32765
  const mod = await import("@hasna/files");
@@ -31453,12 +32768,12 @@ async function persistFile(localPath, opts) {
31453
32768
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
31454
32769
  }
31455
32770
  } catch {}
31456
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
32771
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
31457
32772
  const date = new Date().toISOString().split("T")[0];
31458
- const dir = join6(dataDir, "persistent", date);
31459
- mkdirSync6(dir, { recursive: true });
32773
+ const dir = join7(dataDir, "persistent", date);
32774
+ mkdirSync7(dir, { recursive: true });
31460
32775
  const filename = localPath.split("/").pop() ?? "file";
31461
- const targetPath = join6(dir, filename);
32776
+ const targetPath = join7(dir, filename);
31462
32777
  copyFileSync2(localPath, targetPath);
31463
32778
  return {
31464
32779
  id: `local-${Date.now()}`,
@@ -31468,6 +32783,9 @@ async function persistFile(localPath, opts) {
31468
32783
  };
31469
32784
  }
31470
32785
 
32786
+ // src/mcp/index.ts
32787
+ init_recordings();
32788
+
31471
32789
  // src/db/timeline.ts
31472
32790
  init_schema();
31473
32791
  function logEvent(sessionId, eventType, details = {}) {
@@ -31565,7 +32883,7 @@ async function closeTab(page, index) {
31565
32883
  init_dialogs();
31566
32884
  init_profiles();
31567
32885
  init_types();
31568
- var _pkg = JSON.parse(readFileSync8(join13(import.meta.dir, "../../package.json"), "utf8"));
32886
+ var _pkg = JSON.parse(readFileSync8(join15(import.meta.dir, "../../package.json"), "utf8"));
31569
32887
  var networkLogCleanup = new Map;
31570
32888
  var consoleCaptureCleanup = new Map;
31571
32889
  var harCaptures = new Map;
@@ -31612,7 +32930,7 @@ var server = new McpServer({
31612
32930
  name: "@hasna/browser",
31613
32931
  version: "0.0.1"
31614
32932
  });
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.", {
32933
+ 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
32934
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
31617
32935
  use_case: exports_external.string().optional(),
31618
32936
  project_id: exports_external.string().optional(),
@@ -31623,9 +32941,11 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
31623
32941
  viewport_height: exports_external.number().optional().default(720),
31624
32942
  stealth: exports_external.boolean().optional().default(false),
31625
32943
  auto_gallery: exports_external.boolean().optional().default(false),
32944
+ storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
31626
32945
  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 }) => {
32946
+ tags: exports_external.array(exports_external.string()).optional(),
32947
+ 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")
32948
+ }, 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
32949
  try {
31630
32950
  if (agent_id && !force_new) {
31631
32951
  const existing = getActiveSessionForAgent2(agent_id);
@@ -31641,7 +32961,9 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
31641
32961
  headless,
31642
32962
  viewport: { width: viewport_width, height: viewport_height },
31643
32963
  stealth,
31644
- autoGallery: auto_gallery
32964
+ autoGallery: auto_gallery,
32965
+ storageState: storage_state,
32966
+ cdpUrl: cdp_url
31645
32967
  });
31646
32968
  if (tags?.length) {
31647
32969
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -31801,7 +33123,7 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
31801
33123
  return err(e);
31802
33124
  }
31803
33125
  });
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 }) => {
33126
+ 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
33127
  try {
31806
33128
  const sid = resolveSessionId(session_id);
31807
33129
  const page = getSessionPage(sid);
@@ -31812,14 +33134,17 @@ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS sel
31812
33134
  }
31813
33135
  if (!selector)
31814
33136
  return err(new Error("Either ref or selector is required"));
31815
- await click(page, selector, { button, timeout });
31816
- logEvent(sid, "click", { selector, method: "selector" });
33137
+ const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
33138
+ logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
33139
+ if (healInfo.healed) {
33140
+ return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
33141
+ }
31817
33142
  return json({ clicked: selector, method: "selector" });
31818
33143
  } catch (e) {
31819
33144
  return errWithScreenshot(e, session_id);
31820
33145
  }
31821
33146
  });
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 }) => {
33147
+ 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
33148
  try {
31824
33149
  const sid = resolveSessionId(session_id);
31825
33150
  const page = getSessionPage(sid);
@@ -31830,8 +33155,11 @@ server.tool("browser_type", "Type text into an element by ref or selector. Prefe
31830
33155
  }
31831
33156
  if (!selector)
31832
33157
  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) });
33158
+ const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
33159
+ logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
33160
+ if (healInfo.healed) {
33161
+ return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
33162
+ }
31835
33163
  return json({ typed: text, selector, method: "selector" });
31836
33164
  } catch (e) {
31837
33165
  return errWithScreenshot(e, session_id);
@@ -31925,20 +33253,32 @@ server.tool("browser_wait", "Wait for a selector to appear", { session_id: expor
31925
33253
  return err(e);
31926
33254
  }
31927
33255
  });
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 }) => {
33256
+ 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
33257
  try {
31930
33258
  const sid = resolveSessionId(session_id);
31931
33259
  const page = getSessionPage(sid);
31932
- return json({ text: await getText(page, selector) });
33260
+ const text = await getText(page, selector);
33261
+ if (sanitize) {
33262
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
33263
+ const sanitized = sanitizeText2(text);
33264
+ return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
33265
+ }
33266
+ return json({ text });
31933
33267
  } catch (e) {
31934
33268
  return err(e);
31935
33269
  }
31936
33270
  });
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 }) => {
33271
+ 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
33272
  try {
31939
33273
  const sid = resolveSessionId(session_id);
31940
33274
  const page = getSessionPage(sid);
31941
- return json({ html: await getHTML(page, selector) });
33275
+ const html = await getHTML(page, selector);
33276
+ if (sanitize) {
33277
+ const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
33278
+ const sanitized = sanitizeHTML2(html);
33279
+ return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
33280
+ }
33281
+ return json({ html });
31942
33282
  } catch (e) {
31943
33283
  return err(e);
31944
33284
  }
@@ -31953,16 +33293,32 @@ server.tool("browser_get_links", "Get all links from the current page", { sessio
31953
33293
  return err(e);
31954
33294
  }
31955
33295
  });
31956
- server.tool("browser_extract", "Extract content from the page in a specified format", {
33296
+ server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
31957
33297
  session_id: exports_external.string().optional(),
31958
33298
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
31959
33299
  selector: exports_external.string().optional(),
31960
- schema: exports_external.record(exports_external.string()).optional()
31961
- }, async ({ session_id, format, selector, schema }) => {
33300
+ schema: exports_external.record(exports_external.string()).optional(),
33301
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
33302
+ }, async ({ session_id, format, selector, schema, sanitize }) => {
31962
33303
  try {
31963
33304
  const sid = resolveSessionId(session_id);
31964
33305
  const page = getSessionPage(sid);
31965
33306
  const result = await extract(page, { format, selector, schema });
33307
+ if (sanitize) {
33308
+ const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
33309
+ if (result.text) {
33310
+ const s = sanitizeText2(result.text);
33311
+ result.text = s.text;
33312
+ result.stripped = s.stripped;
33313
+ result.warnings = s.warnings;
33314
+ }
33315
+ if (result.html) {
33316
+ const s = sanitizeHTML2(result.html);
33317
+ result.html = s.text;
33318
+ result.stripped = s.stripped;
33319
+ result.warnings = s.warnings;
33320
+ }
33321
+ }
31966
33322
  return json(result);
31967
33323
  } catch (e) {
31968
33324
  return err(e);
@@ -31979,17 +33335,27 @@ server.tool("browser_find", "Find elements matching a selector and return their
31979
33335
  return err(e);
31980
33336
  }
31981
33337
  });
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.", {
33338
+ 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
33339
  session_id: exports_external.string().optional(),
31984
33340
  compact: exports_external.boolean().optional().default(true),
31985
33341
  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 }) => {
33342
+ full_tree: exports_external.boolean().optional().default(false),
33343
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
33344
+ }, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
31988
33345
  try {
31989
33346
  const sid = resolveSessionId(session_id);
31990
33347
  const page = getSessionPage(sid);
31991
33348
  const result = await takeSnapshot(page, sid);
31992
33349
  setLastSnapshot(sid, result);
33350
+ let injection_warnings;
33351
+ if (sanitize) {
33352
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
33353
+ const sanitized = sanitizeText2(result.tree);
33354
+ if (sanitized.stripped > 0) {
33355
+ injection_warnings = sanitized.warnings;
33356
+ result.tree = sanitized.text;
33357
+ }
33358
+ }
31993
33359
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
31994
33360
  const limitedRefs = Object.fromEntries(refEntries);
31995
33361
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -32001,27 +33367,29 @@ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@
32001
33367
  interactive_count: result.interactive_count,
32002
33368
  shown_count: refEntries.length,
32003
33369
  truncated,
32004
- refs: limitedRefs
33370
+ refs: limitedRefs,
33371
+ ...injection_warnings ? { injection_warnings } : {}
32005
33372
  });
32006
33373
  }
32007
33374
  const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
32008
33375
  ... (truncated \u2014 use full_tree=true for complete)` : "");
32009
- return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
33376
+ return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
32010
33377
  } catch (e) {
32011
33378
  return err(e);
32012
33379
  }
32013
33380
  });
32014
- server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
33381
+ 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
33382
  session_id: exports_external.string().optional(),
32016
- selector: exports_external.string().optional(),
33383
+ selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
32017
33384
  full_page: exports_external.boolean().optional().default(false),
32018
33385
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
32019
33386
  quality: exports_external.number().optional().default(60),
32020
33387
  max_width: exports_external.number().optional().default(800),
32021
33388
  compress: exports_external.boolean().optional().default(true),
32022
33389
  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 }) => {
33390
+ annotate: exports_external.boolean().optional().default(false),
33391
+ 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).")
33392
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
32025
33393
  try {
32026
33394
  const sid = resolveSessionId(session_id);
32027
33395
  const page = getSessionPage(sid);
@@ -32038,7 +33406,9 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
32038
33406
  annotation_count: annotated.annotations.length
32039
33407
  });
32040
33408
  }
32041
- const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
33409
+ const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
33410
+ const effectiveQuality = detail === "high" ? 75 : quality;
33411
+ const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
32042
33412
  result.url = page.url();
32043
33413
  try {
32044
33414
  const buf = Buffer.from(result.base64, "base64");
@@ -32047,12 +33417,12 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
32047
33417
  result.download_id = dl.id;
32048
33418
  } catch {}
32049
33419
  result.estimated_tokens = Math.ceil(result.base64.length / 4);
32050
- if (result.base64.length > 20000) {
33420
+ if (detail !== "high" && result.base64.length > 40000) {
32051
33421
  result.base64_truncated = true;
32052
33422
  result.full_image_path = result.path;
32053
33423
  result.base64 = result.thumbnail_base64 ?? "";
32054
33424
  }
32055
- logEvent(sid, "screenshot", { path: result.path });
33425
+ logEvent(sid, "screenshot", { path: result.path, detail, selector });
32056
33426
  return json(result);
32057
33427
  } catch (e) {
32058
33428
  return err(e);
@@ -32234,6 +33604,28 @@ server.tool("browser_performance", "Get performance metrics for the current page
32234
33604
  return err(e);
32235
33605
  }
32236
33606
  });
33607
+ server.tool("browser_detect_env", "Detect if the current page is running in production, development, staging, or local environment. Analyzes URL, meta tags, source maps, analytics SDKs, and more.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33608
+ try {
33609
+ const sid = resolveSessionId(session_id);
33610
+ const page = getSessionPage(sid);
33611
+ const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
33612
+ const result = await detectEnvironment2(page);
33613
+ return json(result);
33614
+ } catch (e) {
33615
+ return err(e);
33616
+ }
33617
+ });
33618
+ server.tool("browser_performance_deep", "Deep performance analysis: Web Vitals, resource breakdown by type, largest resources, third-party scripts with categories, DOM complexity, memory usage.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33619
+ try {
33620
+ const sid = resolveSessionId(session_id);
33621
+ const page = getSessionPage(sid);
33622
+ const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
33623
+ const result = await getDeepPerformance2(page);
33624
+ return json(result);
33625
+ } catch (e) {
33626
+ return err(e);
33627
+ }
33628
+ });
32237
33629
  server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
32238
33630
  try {
32239
33631
  const sid = resolveSessionId(session_id);
@@ -32297,6 +33689,46 @@ server.tool("browser_recordings_list", "List all recordings", { project_id: expo
32297
33689
  return err(e);
32298
33690
  }
32299
33691
  });
33692
+ server.tool("browser_workflow_save", "Save a recording as a reusable workflow with self-healing replay", { recording_id: exports_external.string(), name: exports_external.string(), description: exports_external.string().optional() }, async ({ recording_id, name, description }) => {
33693
+ try {
33694
+ const { saveWorkflowFromRecording: saveWorkflowFromRecording2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33695
+ return json(saveWorkflowFromRecording2(recording_id, name, description));
33696
+ } catch (e) {
33697
+ return err(e);
33698
+ }
33699
+ });
33700
+ server.tool("browser_workflow_list", "List all saved workflows", {}, async () => {
33701
+ try {
33702
+ const { listWorkflows: listWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33703
+ const workflows = listWorkflows2();
33704
+ return json({ workflows: workflows.map((w) => ({ ...w, steps: `${w.steps.length} steps` })), count: workflows.length });
33705
+ } catch (e) {
33706
+ return err(e);
33707
+ }
33708
+ });
33709
+ server.tool("browser_workflow_run", "Run a saved workflow with self-healing. If selectors changed, auto-adapts and reports what was healed.", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
33710
+ try {
33711
+ const sid = resolveSessionId(session_id);
33712
+ const page = getSessionPage(sid);
33713
+ const { getWorkflowByName: getWorkflowByName2, runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33714
+ const workflow = getWorkflowByName2(name);
33715
+ if (!workflow)
33716
+ return err(new Error(`Workflow '${name}' not found`));
33717
+ const result = await runWorkflow2(workflow, page);
33718
+ logEvent(sid, "workflow_run", { name, ...result });
33719
+ return json(result);
33720
+ } catch (e) {
33721
+ return err(e);
33722
+ }
33723
+ });
33724
+ server.tool("browser_workflow_delete", "Delete a saved workflow", { name: exports_external.string() }, async ({ name }) => {
33725
+ try {
33726
+ const { deleteWorkflow: deleteWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
33727
+ return json({ deleted: deleteWorkflow2(name) });
33728
+ } catch (e) {
33729
+ return err(e);
33730
+ }
33731
+ });
32300
33732
  server.tool("browser_crawl", "Crawl a URL recursively and return discovered pages", {
32301
33733
  url: exports_external.string(),
32302
33734
  max_depth: exports_external.number().optional().default(2),
@@ -32458,6 +33890,94 @@ server.tool("browser_session_untag", "Remove a tag from a session", { session_id
32458
33890
  return err(e);
32459
33891
  }
32460
33892
  });
33893
+ 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 }) => {
33894
+ try {
33895
+ const sid = resolveSessionId(session_id);
33896
+ const page = getSessionPage(sid);
33897
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33898
+ const path = await saveStateFromPage2(page, name);
33899
+ return json({ saved: true, name, path });
33900
+ } catch (e) {
33901
+ return err(e);
33902
+ }
33903
+ });
33904
+ server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
33905
+ try {
33906
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33907
+ const states = listStates2();
33908
+ return json({ states, count: states.length });
33909
+ } catch (e) {
33910
+ return err(e);
33911
+ }
33912
+ });
33913
+ server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
33914
+ try {
33915
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33916
+ return json({ deleted: deleteState2(name), name });
33917
+ } catch (e) {
33918
+ return err(e);
33919
+ }
33920
+ });
33921
+ 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 }) => {
33922
+ try {
33923
+ const sid = resolveSessionId(session_id);
33924
+ const page = getSessionPage(sid);
33925
+ if (start_url)
33926
+ await navigate(page, start_url);
33927
+ const recording = startRecording(sid, `auth-${name}`, page.url());
33928
+ return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
33929
+ } catch (e) {
33930
+ return err(e);
33931
+ }
33932
+ });
33933
+ 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 }) => {
33934
+ try {
33935
+ const sid = resolveSessionId(session_id);
33936
+ const page = getSessionPage(sid);
33937
+ const recording = stopRecording(recording_id);
33938
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
33939
+ const statePath2 = await saveStateFromPage2(page, name);
33940
+ let domain = "";
33941
+ try {
33942
+ domain = new URL(page.url()).hostname;
33943
+ } catch {}
33944
+ const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33945
+ const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
33946
+ return json({ flow, recording_steps: recording.steps.length });
33947
+ } catch (e) {
33948
+ return err(e);
33949
+ }
33950
+ });
33951
+ 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 }) => {
33952
+ try {
33953
+ const sid = resolveSessionId(session_id);
33954
+ const page = getSessionPage(sid);
33955
+ const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33956
+ const flow = getAuthFlowByName2(name);
33957
+ if (!flow)
33958
+ return err(new Error(`Auth flow '${name}' not found`));
33959
+ const result = await tryReplayAuth2(page, flow.domain);
33960
+ return json(result);
33961
+ } catch (e) {
33962
+ return err(e);
33963
+ }
33964
+ });
33965
+ server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
33966
+ try {
33967
+ const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33968
+ return json({ flows: listAuthFlows2() });
33969
+ } catch (e) {
33970
+ return err(e);
33971
+ }
33972
+ });
33973
+ server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
33974
+ try {
33975
+ const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
33976
+ return json({ deleted: deleteAuthFlow2(name) });
33977
+ } catch (e) {
33978
+ return err(e);
33979
+ }
33980
+ });
32461
33981
  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
33982
  try {
32463
33983
  const sid = resolveSessionId(session_id);
@@ -32468,20 +33988,45 @@ server.tool("browser_click_text", "Click an element by its visible text content"
32468
33988
  return err(e);
32469
33989
  }
32470
33990
  });
32471
- server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
33991
+ 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
33992
  session_id: exports_external.string().optional(),
32473
33993
  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 }) => {
33994
+ submit_selector: exports_external.string().optional(),
33995
+ self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
33996
+ }, async ({ session_id, fields, submit_selector, self_heal }) => {
32476
33997
  try {
32477
33998
  const sid = resolveSessionId(session_id);
32478
33999
  const page = getSessionPage(sid);
32479
- const result = await fillForm(page, fields, submit_selector);
34000
+ const result = await fillForm(page, fields, submit_selector, self_heal);
32480
34001
  return json(result);
32481
34002
  } catch (e) {
32482
34003
  return errWithScreenshot(e, session_id);
32483
34004
  }
32484
34005
  });
34006
+ 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.", {
34007
+ session_id: exports_external.string().optional(),
34008
+ 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')"),
34009
+ click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
34010
+ model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
34011
+ }, async ({ session_id, description, click: doClick, model }) => {
34012
+ try {
34013
+ const sid = resolveSessionId(session_id);
34014
+ const page = getSessionPage(sid);
34015
+ if (doClick) {
34016
+ const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
34017
+ const result = await clickByVision2(page, description, { model });
34018
+ logEvent(sid, "vision_click", { query: description, ...result });
34019
+ return json(result);
34020
+ } else {
34021
+ const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
34022
+ const result = await findElementByVision2(page, description, { model });
34023
+ logEvent(sid, "vision_find", { query: description, ...result });
34024
+ return json(result);
34025
+ }
34026
+ } catch (e) {
34027
+ return err(e);
34028
+ }
34029
+ });
32485
34030
  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
34031
  try {
32487
34032
  const sid = resolveSessionId(session_id);
@@ -32879,6 +34424,68 @@ server.tool("browser_profile_delete", "Delete a saved browser profile", { name:
32879
34424
  return err(e);
32880
34425
  }
32881
34426
  });
34427
+ server.tool("browser_detect_apis", "Scan network traffic for JSON API endpoints. Returns discovered endpoints with methods, status codes, and URLs.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34428
+ try {
34429
+ const sid = resolveSessionId(session_id);
34430
+ const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
34431
+ const apis = detectAPIs2(sid);
34432
+ return json({ apis, count: apis.length });
34433
+ } catch (e) {
34434
+ return err(e);
34435
+ }
34436
+ });
34437
+ server.tool("browser_extract_structured", "Extract structured data from page: tables, lists, JSON-LD, Open Graph, meta tags, and repeated elements (cards/items).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34438
+ try {
34439
+ const sid = resolveSessionId(session_id);
34440
+ const page = getSessionPage(sid);
34441
+ const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
34442
+ const data = await extractStructuredData2(page);
34443
+ return json({
34444
+ tables: data.tables.length,
34445
+ lists: data.lists.length,
34446
+ json_ld: data.jsonLd.length,
34447
+ open_graph: Object.keys(data.openGraph).length,
34448
+ meta_tags: Object.keys(data.metaTags).length,
34449
+ repeated_elements: data.repeatedElements.length,
34450
+ data
34451
+ });
34452
+ } catch (e) {
34453
+ return err(e);
34454
+ }
34455
+ });
34456
+ server.tool("browser_dataset_save", "Save extracted data as a named dataset for later use", { name: exports_external.string(), data: exports_external.array(exports_external.record(exports_external.unknown())), source_url: exports_external.string().optional() }, async ({ name, data, source_url }) => {
34457
+ try {
34458
+ const { saveDataset: saveDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34459
+ const dataset = saveDataset2({ name, rows: data, sourceUrl: source_url });
34460
+ return json({ id: dataset.id, name: dataset.name, row_count: dataset.row_count });
34461
+ } catch (e) {
34462
+ return err(e);
34463
+ }
34464
+ });
34465
+ server.tool("browser_dataset_list", "List all saved datasets", {}, async () => {
34466
+ try {
34467
+ const { listDatasets: listDatasets2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34468
+ return json({ datasets: listDatasets2() });
34469
+ } catch (e) {
34470
+ return err(e);
34471
+ }
34472
+ });
34473
+ server.tool("browser_dataset_export", "Export a dataset as JSON or CSV file", { name: exports_external.string(), format: exports_external.enum(["json", "csv"]).optional().default("json") }, async ({ name, format }) => {
34474
+ try {
34475
+ const { exportDataset: exportDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34476
+ return json(exportDataset2(name, format));
34477
+ } catch (e) {
34478
+ return err(e);
34479
+ }
34480
+ });
34481
+ server.tool("browser_dataset_delete", "Delete a saved dataset", { name: exports_external.string() }, async ({ name }) => {
34482
+ try {
34483
+ const { deleteDataset: deleteDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
34484
+ return json({ deleted: deleteDataset2(name) });
34485
+ } catch (e) {
34486
+ return err(e);
34487
+ }
34488
+ });
32882
34489
  server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
32883
34490
  try {
32884
34491
  const groups = {
@@ -32902,6 +34509,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32902
34509
  { tool: "browser_wait", description: "Wait for a selector to appear" },
32903
34510
  { tool: "browser_wait_for_text", description: "Wait for text to appear" },
32904
34511
  { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
34512
+ { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
32905
34513
  { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
32906
34514
  ],
32907
34515
  Extraction: [
@@ -32930,7 +34538,10 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32930
34538
  { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
32931
34539
  { tool: "browser_profile_load", description: "Load and apply a saved profile" },
32932
34540
  { tool: "browser_profile_list", description: "List saved profiles" },
32933
- { tool: "browser_profile_delete", description: "Delete a saved profile" }
34541
+ { tool: "browser_profile_delete", description: "Delete a saved profile" },
34542
+ { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
34543
+ { tool: "browser_session_list_states", description: "List saved storage states" },
34544
+ { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
32934
34545
  ],
32935
34546
  Network: [
32936
34547
  { tool: "browser_network_log", description: "Get captured network requests" },
@@ -32954,6 +34565,27 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
32954
34565
  { tool: "browser_record_replay", description: "Replay a recorded sequence" },
32955
34566
  { tool: "browser_recordings_list", description: "List all recordings" }
32956
34567
  ],
34568
+ Auth: [
34569
+ { tool: "browser_auth_record", description: "Start recording a login flow" },
34570
+ { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
34571
+ { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
34572
+ { tool: "browser_auth_list", description: "List all saved auth flows" },
34573
+ { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
34574
+ ],
34575
+ Workflows: [
34576
+ { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
34577
+ { tool: "browser_workflow_list", description: "List all saved workflows" },
34578
+ { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
34579
+ { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
34580
+ ],
34581
+ Data: [
34582
+ { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
34583
+ { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
34584
+ { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
34585
+ { tool: "browser_dataset_list", description: "List all saved datasets" },
34586
+ { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
34587
+ { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
34588
+ ],
32957
34589
  Crawl: [
32958
34590
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
32959
34591
  ],
@@ -33007,10 +34639,13 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
33007
34639
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
33008
34640
  { tool: "browser_version", description: "Show running binary version and tool count" },
33009
34641
  { tool: "browser_help", description: "Show this help (all tools)" },
34642
+ { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
34643
+ { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
33010
34644
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
33011
34645
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
33012
34646
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
33013
- { tool: "browser_watch_stop", description: "Stop DOM watcher" }
34647
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" },
34648
+ { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
33014
34649
  ]
33015
34650
  };
33016
34651
  const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
@@ -33293,6 +34928,82 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
33293
34928
  return err(e);
33294
34929
  }
33295
34930
  });
34931
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
34932
+ actions: exports_external.array(exports_external.object({
34933
+ session_id: exports_external.string().describe("Target session ID"),
34934
+ tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
34935
+ args: exports_external.record(exports_external.unknown()).optional().default({})
34936
+ })),
34937
+ timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
34938
+ }, async ({ actions, timeout }) => {
34939
+ try {
34940
+ const t0 = Date.now();
34941
+ const promises = actions.map(async (action, index) => {
34942
+ try {
34943
+ const sid = action.session_id;
34944
+ const page = getSessionPage(sid);
34945
+ const args = action.args;
34946
+ const toolName = action.tool.replace(/^browser_/, "");
34947
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
34948
+ const actionPromise = (async () => {
34949
+ switch (toolName) {
34950
+ case "navigate": {
34951
+ await navigate(page, args.url);
34952
+ const title = await page.title();
34953
+ return { url: page.url(), title };
34954
+ }
34955
+ case "screenshot": {
34956
+ const result2 = await takeScreenshot(page, {
34957
+ maxWidth: args.max_width ?? 800,
34958
+ quality: args.quality ?? 60
34959
+ });
34960
+ return { path: result2.path, size_bytes: result2.size_bytes };
34961
+ }
34962
+ case "click": {
34963
+ if (args.selector)
34964
+ await click(page, args.selector);
34965
+ return { clicked: args.selector };
34966
+ }
34967
+ case "type": {
34968
+ if (args.selector && args.text)
34969
+ await type(page, args.selector, args.text);
34970
+ return { typed: args.text };
34971
+ }
34972
+ case "get_text": {
34973
+ const text = await getText(page);
34974
+ return { text: text.slice(0, 1000), length: text.length };
34975
+ }
34976
+ case "get_links": {
34977
+ const links = await getLinks(page);
34978
+ return { links, count: links.length };
34979
+ }
34980
+ case "snapshot": {
34981
+ const snap = await takeSnapshot(page, sid);
34982
+ return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
34983
+ }
34984
+ case "evaluate": {
34985
+ const result2 = await page.evaluate(args.expression);
34986
+ return { result: result2 };
34987
+ }
34988
+ default:
34989
+ return { error: `Unknown tool: ${action.tool}` };
34990
+ }
34991
+ })();
34992
+ const result = await Promise.race([actionPromise, timeoutPromise]);
34993
+ return { index, session_id: sid, tool: action.tool, success: true, result };
34994
+ } catch (e) {
34995
+ return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
34996
+ }
34997
+ });
34998
+ const results = await Promise.all(promises);
34999
+ const duration_ms = Date.now() - t0;
35000
+ const succeeded = results.filter((r) => r.success).length;
35001
+ const failed = results.filter((r) => !r.success).length;
35002
+ return json({ results, duration_ms, succeeded, failed, total: actions.length });
35003
+ } catch (e) {
35004
+ return err(e);
35005
+ }
35006
+ });
33296
35007
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
33297
35008
  try {
33298
35009
  return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });