@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/cli/index.js CHANGED
@@ -2341,6 +2341,71 @@ function runMigrations(db) {
2341
2341
  );
2342
2342
  CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
2343
2343
  `
2344
+ },
2345
+ {
2346
+ version: 6,
2347
+ sql: `
2348
+ CREATE TABLE IF NOT EXISTS auth_flows (
2349
+ id TEXT PRIMARY KEY,
2350
+ name TEXT NOT NULL UNIQUE,
2351
+ domain TEXT NOT NULL,
2352
+ recording_id TEXT REFERENCES recordings(id),
2353
+ storage_state_path TEXT,
2354
+ created_at TEXT DEFAULT (datetime('now')),
2355
+ last_used TEXT
2356
+ );
2357
+
2358
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
2359
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
2360
+ `
2361
+ },
2362
+ {
2363
+ version: 7,
2364
+ sql: `
2365
+ CREATE TABLE IF NOT EXISTS workflows (
2366
+ id TEXT PRIMARY KEY,
2367
+ name TEXT NOT NULL UNIQUE,
2368
+ description TEXT,
2369
+ steps TEXT NOT NULL DEFAULT '[]',
2370
+ start_url TEXT,
2371
+ last_run TEXT,
2372
+ last_heal TEXT,
2373
+ heal_count INTEGER DEFAULT 0,
2374
+ run_count INTEGER DEFAULT 0,
2375
+ created_at TEXT DEFAULT (datetime('now')),
2376
+ updated_at TEXT DEFAULT (datetime('now'))
2377
+ );
2378
+ `
2379
+ },
2380
+ {
2381
+ version: 8,
2382
+ sql: `
2383
+ CREATE TABLE IF NOT EXISTS datasets (
2384
+ id TEXT PRIMARY KEY,
2385
+ name TEXT NOT NULL UNIQUE,
2386
+ source_url TEXT,
2387
+ source_type TEXT NOT NULL DEFAULT 'page',
2388
+ data TEXT NOT NULL DEFAULT '[]',
2389
+ schema TEXT,
2390
+ row_count INTEGER DEFAULT 0,
2391
+ last_refresh TEXT,
2392
+ created_at TEXT DEFAULT (datetime('now')),
2393
+ updated_at TEXT DEFAULT (datetime('now'))
2394
+ );
2395
+
2396
+ CREATE TABLE IF NOT EXISTS api_endpoints (
2397
+ id TEXT PRIMARY KEY,
2398
+ session_id TEXT,
2399
+ url TEXT NOT NULL,
2400
+ method TEXT DEFAULT 'GET',
2401
+ response_schema TEXT,
2402
+ sample_response TEXT,
2403
+ status_code INTEGER,
2404
+ content_type TEXT,
2405
+ discovered_at TEXT DEFAULT (datetime('now'))
2406
+ );
2407
+ CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
2408
+ `
2344
2409
  }
2345
2410
  ];
2346
2411
  for (const m of migrations) {
@@ -3305,14 +3370,14 @@ function enableConsoleCapture(page, sessionId) {
3305
3370
  warning: "warn"
3306
3371
  };
3307
3372
  const level = levelMap[msg.type()] ?? "log";
3308
- const location = msg.location();
3373
+ const location2 = msg.location();
3309
3374
  try {
3310
3375
  logConsoleMessage({
3311
3376
  session_id: sessionId,
3312
3377
  level,
3313
3378
  message: msg.text(),
3314
- source: location.url || undefined,
3315
- line_number: location.lineNumber || undefined
3379
+ source: location2.url || undefined,
3380
+ line_number: location2.lineNumber || undefined
3316
3381
  });
3317
3382
  } catch {}
3318
3383
  };
@@ -3456,6 +3521,188 @@ var init_dialogs = __esm(() => {
3456
3521
  pendingDialogs = new Map;
3457
3522
  });
3458
3523
 
3524
+ // src/engines/cdp.ts
3525
+ var exports_cdp = {};
3526
+ __export(exports_cdp, {
3527
+ connectToExistingBrowser: () => connectToExistingBrowser,
3528
+ CDPClient: () => CDPClient
3529
+ });
3530
+ async function connectToExistingBrowser(cdpUrl) {
3531
+ const { chromium: chromium3 } = await import("playwright");
3532
+ try {
3533
+ return await chromium3.connectOverCDP(cdpUrl);
3534
+ } catch (err) {
3535
+ 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);
3536
+ }
3537
+ }
3538
+
3539
+ class CDPClient {
3540
+ session;
3541
+ networkEnabled = false;
3542
+ performanceEnabled = false;
3543
+ constructor(session) {
3544
+ this.session = session;
3545
+ }
3546
+ static async fromPage(page) {
3547
+ try {
3548
+ const session = await page.context().newCDPSession(page);
3549
+ return new CDPClient(session);
3550
+ } catch (err) {
3551
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
3552
+ }
3553
+ }
3554
+ async send(method, params) {
3555
+ try {
3556
+ return await this.session.send(method, params);
3557
+ } catch (err) {
3558
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
3559
+ }
3560
+ }
3561
+ on(event, handler) {
3562
+ this.session.on(event, handler);
3563
+ }
3564
+ off(event, handler) {
3565
+ this.session.off(event, handler);
3566
+ }
3567
+ async enableNetwork() {
3568
+ if (!this.networkEnabled) {
3569
+ await this.send("Network.enable");
3570
+ this.networkEnabled = true;
3571
+ }
3572
+ }
3573
+ async enablePerformance() {
3574
+ if (!this.performanceEnabled) {
3575
+ await this.send("Performance.enable");
3576
+ this.performanceEnabled = true;
3577
+ }
3578
+ }
3579
+ async getPerformanceMetrics() {
3580
+ await this.enablePerformance();
3581
+ const result = await this.send("Performance.getMetrics");
3582
+ const m = {};
3583
+ for (const metric of result.metrics) {
3584
+ m[metric.name] = metric.value;
3585
+ }
3586
+ return {
3587
+ js_heap_size_used: m["JSHeapUsedSize"],
3588
+ js_heap_size_total: m["JSHeapTotalSize"],
3589
+ dom_interactive: m["DOMInteractive"],
3590
+ dom_complete: m["DOMComplete"],
3591
+ load_event: m["LoadEventEnd"]
3592
+ };
3593
+ }
3594
+ async startJSCoverage() {
3595
+ await this.send("Profiler.enable");
3596
+ await this.send("Debugger.enable");
3597
+ await this.send("Profiler.startPreciseCoverage", {
3598
+ callCount: false,
3599
+ detailed: true
3600
+ });
3601
+ }
3602
+ async stopJSCoverage() {
3603
+ const result = await this.send("Profiler.takePreciseCoverage");
3604
+ await this.send("Profiler.stopPreciseCoverage");
3605
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
3606
+ url: r.url,
3607
+ text: "",
3608
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
3609
+ }));
3610
+ }
3611
+ async getCoverage() {
3612
+ await this.startJSCoverage();
3613
+ const js = await this.stopJSCoverage();
3614
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
3615
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
3616
+ }
3617
+ async captureHAREntries(page, handler) {
3618
+ await this.enableNetwork();
3619
+ const requestTimings = new Map;
3620
+ const onRequest = (params) => {
3621
+ requestTimings.set(params.requestId, params.timestamp);
3622
+ };
3623
+ const onResponse = (params) => {
3624
+ const start = requestTimings.get(params.requestId);
3625
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
3626
+ handler({
3627
+ method: "GET",
3628
+ url: params.response.url,
3629
+ status: params.response.status,
3630
+ duration
3631
+ });
3632
+ };
3633
+ this.on("Network.requestWillBeSent", onRequest);
3634
+ this.on("Network.responseReceived", onResponse);
3635
+ return () => {
3636
+ this.off("Network.requestWillBeSent", onRequest);
3637
+ this.off("Network.responseReceived", onResponse);
3638
+ };
3639
+ }
3640
+ async detach() {
3641
+ try {
3642
+ await this.session.detach();
3643
+ } catch {}
3644
+ }
3645
+ }
3646
+ var init_cdp = __esm(() => {
3647
+ init_types();
3648
+ });
3649
+
3650
+ // src/lib/storage-state.ts
3651
+ var exports_storage_state = {};
3652
+ __export(exports_storage_state, {
3653
+ saveStateFromPage: () => saveStateFromPage,
3654
+ saveState: () => saveState,
3655
+ loadStatePath: () => loadStatePath,
3656
+ listStates: () => listStates,
3657
+ deleteState: () => deleteState
3658
+ });
3659
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
3660
+ import { join as join3 } from "path";
3661
+ import { homedir as homedir3 } from "os";
3662
+ function ensureDir() {
3663
+ mkdirSync3(STATES_DIR, { recursive: true });
3664
+ }
3665
+ function statePath(name) {
3666
+ return join3(STATES_DIR, `${name}.json`);
3667
+ }
3668
+ async function saveState(context, name) {
3669
+ ensureDir();
3670
+ const path = statePath(name);
3671
+ const state = await context.storageState({ path });
3672
+ return path;
3673
+ }
3674
+ async function saveStateFromPage(page, name) {
3675
+ return saveState(page.context(), name);
3676
+ }
3677
+ function loadStatePath(name) {
3678
+ const path = statePath(name);
3679
+ return existsSync(path) ? path : null;
3680
+ }
3681
+ function listStates() {
3682
+ ensureDir();
3683
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
3684
+ const path = join3(STATES_DIR, f);
3685
+ const stat = Bun.file(path);
3686
+ return {
3687
+ name: f.replace(".json", ""),
3688
+ path,
3689
+ modified: new Date(stat.lastModified).toISOString()
3690
+ };
3691
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
3692
+ }
3693
+ function deleteState(name) {
3694
+ const path = statePath(name);
3695
+ if (existsSync(path)) {
3696
+ unlinkSync(path);
3697
+ return true;
3698
+ }
3699
+ return false;
3700
+ }
3701
+ var STATES_DIR;
3702
+ var init_storage_state = __esm(() => {
3703
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
3704
+ });
3705
+
3459
3706
  // src/lib/session.ts
3460
3707
  var exports_session = {};
3461
3708
  __export(exports_session, {
@@ -3485,6 +3732,37 @@ function createBunProxy(view) {
3485
3732
  return view;
3486
3733
  }
3487
3734
  async function createSession2(opts = {}) {
3735
+ if (opts.cdpUrl) {
3736
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
3737
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
3738
+ const contexts = cdpBrowser.contexts();
3739
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
3740
+ const pages = context.pages();
3741
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
3742
+ const session2 = createSession({
3743
+ engine: "cdp",
3744
+ projectId: opts.projectId,
3745
+ agentId: opts.agentId,
3746
+ startUrl: page2.url(),
3747
+ name: opts.name ?? "attached"
3748
+ });
3749
+ const cleanups2 = [];
3750
+ if (opts.captureNetwork !== false) {
3751
+ try {
3752
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
3753
+ } catch {}
3754
+ }
3755
+ if (opts.captureConsole !== false) {
3756
+ try {
3757
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
3758
+ } catch {}
3759
+ }
3760
+ try {
3761
+ cleanups2.push(setupDialogHandler(page2, session2.id));
3762
+ } catch {}
3763
+ 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 });
3764
+ return { session: session2, page: page2 };
3765
+ }
3488
3766
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
3489
3767
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
3490
3768
  let browser = null;
@@ -3510,7 +3788,22 @@ async function createSession2(opts = {}) {
3510
3788
  page = await context.newPage();
3511
3789
  } else {
3512
3790
  browser = await pool.acquire(opts.headless ?? true);
3513
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3791
+ if (opts.storageState) {
3792
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
3793
+ const statePath2 = loadStatePath2(opts.storageState);
3794
+ if (statePath2) {
3795
+ const context = await browser.newContext({
3796
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
3797
+ userAgent: opts.userAgent,
3798
+ storageState: statePath2
3799
+ });
3800
+ page = await context.newPage();
3801
+ } else {
3802
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3803
+ }
3804
+ } else {
3805
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3806
+ }
3514
3807
  }
3515
3808
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
3516
3809
  try {
@@ -4015,6 +4308,66 @@ var init_snapshot = __esm(() => {
4015
4308
  ];
4016
4309
  });
4017
4310
 
4311
+ // src/lib/self-heal.ts
4312
+ async function healSelector(page, selector, sessionId) {
4313
+ const attempts = [];
4314
+ attempts.push(`selector: ${selector}`);
4315
+ try {
4316
+ const loc = page.locator(selector).first();
4317
+ if (await loc.count() > 0) {
4318
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
4319
+ }
4320
+ } catch {}
4321
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
4322
+ attempts.push(`text: "${selector}"`);
4323
+ try {
4324
+ const loc = page.getByText(selector, { exact: false }).first();
4325
+ if (await loc.count() > 0) {
4326
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
4327
+ }
4328
+ } catch {}
4329
+ }
4330
+ const roleMap = {
4331
+ button: ["button", "submit", "reset"],
4332
+ link: ["a"],
4333
+ input: ["input", "textarea"],
4334
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
4335
+ };
4336
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
4337
+ for (const [role, tags] of Object.entries(roleMap)) {
4338
+ attempts.push(`role: ${role} name~="${nameHint}"`);
4339
+ try {
4340
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
4341
+ if (await loc.count() > 0) {
4342
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
4343
+ }
4344
+ } catch {}
4345
+ }
4346
+ if (selector.startsWith("#")) {
4347
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
4348
+ const partialSel = `[id*="${idPart}"]`;
4349
+ attempts.push(`partial_id: ${partialSel}`);
4350
+ try {
4351
+ const loc = page.locator(partialSel).first();
4352
+ if (await loc.count() > 0) {
4353
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
4354
+ }
4355
+ } catch {}
4356
+ }
4357
+ if (selector.startsWith(".")) {
4358
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
4359
+ const partialSel = `[class*="${classPart}"]`;
4360
+ attempts.push(`partial_class: ${partialSel}`);
4361
+ try {
4362
+ const loc = page.locator(partialSel).first();
4363
+ if (await loc.count() > 0) {
4364
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
4365
+ }
4366
+ } catch {}
4367
+ }
4368
+ return { found: false, locator: null, method: "none", healed: false, attempts };
4369
+ }
4370
+
4018
4371
  // src/lib/actions.ts
4019
4372
  var exports_actions = {};
4020
4373
  __export(exports_actions, {
@@ -4056,11 +4409,22 @@ async function click(page, selector, opts) {
4056
4409
  delay: opts?.delay,
4057
4410
  timeout: opts?.timeout ?? 1e4
4058
4411
  });
4059
- } catch (err) {
4060
- if (err instanceof Error && err.message.includes("not found")) {
4412
+ return {};
4413
+ } catch (originalError) {
4414
+ if (opts?.selfHeal !== false) {
4415
+ const result = await healSelector(page, selector);
4416
+ if (result.found && result.locator) {
4417
+ await result.locator.click({
4418
+ button: opts?.button ?? "left",
4419
+ timeout: opts?.timeout ?? 1e4
4420
+ });
4421
+ return { healed: true, method: result.method, attempts: result.attempts };
4422
+ }
4423
+ }
4424
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
4061
4425
  throw new ElementNotFoundError(selector);
4062
4426
  }
4063
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
4427
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
4064
4428
  }
4065
4429
  }
4066
4430
  async function type(page, selector, text, opts) {
@@ -4069,17 +4433,35 @@ async function type(page, selector, text, opts) {
4069
4433
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
4070
4434
  }
4071
4435
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
4072
- } catch (err) {
4073
- if (err instanceof Error && err.message.includes("not found")) {
4436
+ return {};
4437
+ } catch (originalError) {
4438
+ if (opts?.selfHeal !== false) {
4439
+ const result = await healSelector(page, selector);
4440
+ if (result.found && result.locator) {
4441
+ if (opts?.clear)
4442
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
4443
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
4444
+ return { healed: true, method: result.method, attempts: result.attempts };
4445
+ }
4446
+ }
4447
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
4074
4448
  throw new ElementNotFoundError(selector);
4075
4449
  }
4076
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
4450
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
4077
4451
  }
4078
4452
  }
4079
- async function fill(page, selector, value, timeout = 1e4) {
4453
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
4080
4454
  try {
4081
4455
  await page.fill(selector, value, { timeout });
4082
- } catch (err) {
4456
+ return {};
4457
+ } catch (originalError) {
4458
+ if (selfHeal) {
4459
+ const result = await healSelector(page, selector);
4460
+ if (result.found && result.locator) {
4461
+ await result.locator.fill(value, { timeout });
4462
+ return { healed: true, method: result.method, attempts: result.attempts };
4463
+ }
4464
+ }
4083
4465
  throw new ElementNotFoundError(selector);
4084
4466
  }
4085
4467
  }
@@ -4204,12 +4586,39 @@ async function clickText(page, text, opts) {
4204
4586
  }
4205
4587
  }, { retries: opts?.retries ?? 1 });
4206
4588
  }
4207
- async function fillForm(page, fields, submitSelector) {
4589
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
4208
4590
  let filled = 0;
4209
4591
  const errors = [];
4592
+ const healedFields = [];
4210
4593
  for (const [selector, value] of Object.entries(fields)) {
4211
4594
  try {
4212
- const el = await page.$(selector);
4595
+ let el = await page.$(selector);
4596
+ if (!el && selfHeal) {
4597
+ const result = await healSelector(page, selector);
4598
+ if (result.found && result.locator) {
4599
+ const handle = await result.locator.elementHandle();
4600
+ if (handle) {
4601
+ el = handle;
4602
+ healedFields.push(selector);
4603
+ const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
4604
+ const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
4605
+ if (tagName2 === "select") {
4606
+ await result.locator.selectOption(String(value));
4607
+ } else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
4608
+ if (Boolean(value))
4609
+ await result.locator.check();
4610
+ else
4611
+ await result.locator.uncheck();
4612
+ } else {
4613
+ await result.locator.fill(String(value));
4614
+ }
4615
+ filled++;
4616
+ continue;
4617
+ }
4618
+ }
4619
+ errors.push(`${selector}: element not found`);
4620
+ continue;
4621
+ }
4213
4622
  if (!el) {
4214
4623
  errors.push(`${selector}: element not found`);
4215
4624
  continue;
@@ -4236,11 +4645,21 @@ async function fillForm(page, fields, submitSelector) {
4236
4645
  if (submitSelector) {
4237
4646
  try {
4238
4647
  await page.click(submitSelector);
4239
- } catch (err) {
4240
- errors.push(`submit(${submitSelector}): ${err instanceof Error ? err.message : String(err)}`);
4648
+ } catch (submitErr) {
4649
+ if (selfHeal) {
4650
+ const result = await healSelector(page, submitSelector);
4651
+ if (result.found && result.locator) {
4652
+ await result.locator.click();
4653
+ healedFields.push(submitSelector);
4654
+ } else {
4655
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
4656
+ }
4657
+ } else {
4658
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
4659
+ }
4241
4660
  }
4242
4661
  }
4243
- return { filled, errors, fields_attempted: Object.keys(fields).length };
4662
+ return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
4244
4663
  }
4245
4664
  async function waitForText(page, text, opts) {
4246
4665
  const timeout = opts?.timeout ?? 1e4;
@@ -11096,17 +11515,17 @@ var init_gallery = __esm(() => {
11096
11515
  });
11097
11516
 
11098
11517
  // src/lib/screenshot.ts
11099
- import { join as join3 } from "path";
11100
- import { mkdirSync as mkdirSync3 } from "fs";
11101
- import { homedir as homedir3 } from "os";
11518
+ import { join as join4 } from "path";
11519
+ import { mkdirSync as mkdirSync4 } from "fs";
11520
+ import { homedir as homedir4 } from "os";
11102
11521
  function getDataDir2() {
11103
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
11522
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
11104
11523
  }
11105
11524
  function getScreenshotDir(projectId) {
11106
- const base = join3(getDataDir2(), "screenshots");
11525
+ const base = join4(getDataDir2(), "screenshots");
11107
11526
  const date = new Date().toISOString().split("T")[0];
11108
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
11109
- mkdirSync3(dir, { recursive: true });
11527
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
11528
+ mkdirSync4(dir, { recursive: true });
11110
11529
  return dir;
11111
11530
  }
11112
11531
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -11121,7 +11540,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
11121
11540
  }
11122
11541
  }
11123
11542
  async function generateThumbnail(raw, dir, stem) {
11124
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
11543
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
11125
11544
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
11126
11545
  await Bun.write(thumbPath, thumbBuffer);
11127
11546
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -11178,7 +11597,7 @@ async function takeScreenshot(page, opts) {
11178
11597
  const compressedSizeBytes = finalBuffer.length;
11179
11598
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
11180
11599
  const ext = format;
11181
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
11600
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
11182
11601
  await Bun.write(screenshotPath, finalBuffer);
11183
11602
  let thumbnailPath;
11184
11603
  let thumbnailBase64;
@@ -11238,12 +11657,12 @@ async function takeScreenshot(page, opts) {
11238
11657
  }
11239
11658
  async function generatePDF(page, opts) {
11240
11659
  try {
11241
- const base = join3(getDataDir2(), "pdfs");
11660
+ const base = join4(getDataDir2(), "pdfs");
11242
11661
  const date = new Date().toISOString().split("T")[0];
11243
- const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
11244
- mkdirSync3(dir, { recursive: true });
11662
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
11663
+ mkdirSync4(dir, { recursive: true });
11245
11664
  const timestamp = Date.now();
11246
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
11665
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
11247
11666
  const buffer = await page.pdf({
11248
11667
  path: pdfPath,
11249
11668
  format: opts?.format ?? "A4",
@@ -11570,6 +11989,17 @@ var init_recordings = __esm(() => {
11570
11989
  });
11571
11990
 
11572
11991
  // src/lib/recorder.ts
11992
+ var exports_recorder = {};
11993
+ __export(exports_recorder, {
11994
+ stopRecording: () => stopRecording,
11995
+ startRecording: () => startRecording,
11996
+ replayRecording: () => replayRecording,
11997
+ recordStep: () => recordStep,
11998
+ listRecordings: () => listRecordings,
11999
+ getRecording: () => getRecording,
12000
+ exportRecording: () => exportRecording,
12001
+ attachPageListeners: () => attachPageListeners
12002
+ });
11573
12003
  function startRecording(sessionId, name, startUrl) {
11574
12004
  const steps = [];
11575
12005
  const recording = createRecording({ name, start_url: startUrl, steps });
@@ -11580,6 +12010,23 @@ function startRecording(sessionId, name, startUrl) {
11580
12010
  });
11581
12011
  return recording;
11582
12012
  }
12013
+ function attachPageListeners(page, recordingId) {
12014
+ const active = activeRecordings.get(recordingId);
12015
+ if (!active)
12016
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
12017
+ const onFrameNav = () => {
12018
+ active.steps.push({
12019
+ type: "navigate",
12020
+ url: page.url(),
12021
+ timestamp: Date.now()
12022
+ });
12023
+ };
12024
+ page.on("framenavigated", onFrameNav);
12025
+ const cleanup = () => {
12026
+ page.off("framenavigated", onFrameNav);
12027
+ };
12028
+ active.cleanup = cleanup;
12029
+ }
11583
12030
  function recordStep(recordingId, step) {
11584
12031
  const active = activeRecordings.get(recordingId);
11585
12032
  if (!active)
@@ -11651,6 +12098,64 @@ async function replayRecording(recordingId, page) {
11651
12098
  duration_ms: Date.now() - startTime
11652
12099
  };
11653
12100
  }
12101
+ function exportRecording(recordingId, format = "json") {
12102
+ const recording = getRecording(recordingId);
12103
+ if (format === "json") {
12104
+ return JSON.stringify(recording, null, 2);
12105
+ }
12106
+ if (format === "playwright") {
12107
+ const lines2 = [
12108
+ `import { test, expect } from '@playwright/test';`,
12109
+ ``,
12110
+ `test('${recording.name}', async ({ page }) => {`
12111
+ ];
12112
+ for (const step of recording.steps) {
12113
+ switch (step.type) {
12114
+ case "navigate":
12115
+ lines2.push(` await page.goto('${step.url}');`);
12116
+ break;
12117
+ case "click":
12118
+ lines2.push(` await page.click('${step.selector}');`);
12119
+ break;
12120
+ case "type":
12121
+ lines2.push(` await page.type('${step.selector}', '${step.value}');`);
12122
+ break;
12123
+ case "scroll":
12124
+ lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
12125
+ break;
12126
+ case "evaluate":
12127
+ lines2.push(` await page.evaluate(${step.value});`);
12128
+ break;
12129
+ }
12130
+ }
12131
+ lines2.push(`});`);
12132
+ return lines2.join(`
12133
+ `);
12134
+ }
12135
+ const lines = [
12136
+ `const puppeteer = require('puppeteer');`,
12137
+ ``,
12138
+ `(async () => {`,
12139
+ ` const browser = await puppeteer.launch();`,
12140
+ ` const page = await browser.newPage();`
12141
+ ];
12142
+ for (const step of recording.steps) {
12143
+ switch (step.type) {
12144
+ case "navigate":
12145
+ lines.push(` await page.goto('${step.url}');`);
12146
+ break;
12147
+ case "click":
12148
+ lines.push(` await page.click('${step.selector}');`);
12149
+ break;
12150
+ case "type":
12151
+ lines.push(` await page.type('${step.selector}', '${step.value}');`);
12152
+ break;
12153
+ }
12154
+ }
12155
+ lines.push(` await browser.close();`, `})();`);
12156
+ return lines.join(`
12157
+ `);
12158
+ }
11654
12159
  var activeRecordings;
11655
12160
  var init_recorder = __esm(() => {
11656
12161
  init_recordings();
@@ -15625,118 +16130,6 @@ var init_zod = __esm(() => {
15625
16130
  init_external();
15626
16131
  });
15627
16132
 
15628
- // src/engines/cdp.ts
15629
- class CDPClient {
15630
- session;
15631
- networkEnabled = false;
15632
- performanceEnabled = false;
15633
- constructor(session) {
15634
- this.session = session;
15635
- }
15636
- static async fromPage(page) {
15637
- try {
15638
- const session = await page.context().newCDPSession(page);
15639
- return new CDPClient(session);
15640
- } catch (err) {
15641
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
15642
- }
15643
- }
15644
- async send(method, params) {
15645
- try {
15646
- return await this.session.send(method, params);
15647
- } catch (err) {
15648
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
15649
- }
15650
- }
15651
- on(event, handler) {
15652
- this.session.on(event, handler);
15653
- }
15654
- off(event, handler) {
15655
- this.session.off(event, handler);
15656
- }
15657
- async enableNetwork() {
15658
- if (!this.networkEnabled) {
15659
- await this.send("Network.enable");
15660
- this.networkEnabled = true;
15661
- }
15662
- }
15663
- async enablePerformance() {
15664
- if (!this.performanceEnabled) {
15665
- await this.send("Performance.enable");
15666
- this.performanceEnabled = true;
15667
- }
15668
- }
15669
- async getPerformanceMetrics() {
15670
- await this.enablePerformance();
15671
- const result = await this.send("Performance.getMetrics");
15672
- const m = {};
15673
- for (const metric of result.metrics) {
15674
- m[metric.name] = metric.value;
15675
- }
15676
- return {
15677
- js_heap_size_used: m["JSHeapUsedSize"],
15678
- js_heap_size_total: m["JSHeapTotalSize"],
15679
- dom_interactive: m["DOMInteractive"],
15680
- dom_complete: m["DOMComplete"],
15681
- load_event: m["LoadEventEnd"]
15682
- };
15683
- }
15684
- async startJSCoverage() {
15685
- await this.send("Profiler.enable");
15686
- await this.send("Debugger.enable");
15687
- await this.send("Profiler.startPreciseCoverage", {
15688
- callCount: false,
15689
- detailed: true
15690
- });
15691
- }
15692
- async stopJSCoverage() {
15693
- const result = await this.send("Profiler.takePreciseCoverage");
15694
- await this.send("Profiler.stopPreciseCoverage");
15695
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
15696
- url: r.url,
15697
- text: "",
15698
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
15699
- }));
15700
- }
15701
- async getCoverage() {
15702
- await this.startJSCoverage();
15703
- const js = await this.stopJSCoverage();
15704
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
15705
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
15706
- }
15707
- async captureHAREntries(page, handler) {
15708
- await this.enableNetwork();
15709
- const requestTimings = new Map;
15710
- const onRequest = (params) => {
15711
- requestTimings.set(params.requestId, params.timestamp);
15712
- };
15713
- const onResponse = (params) => {
15714
- const start = requestTimings.get(params.requestId);
15715
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
15716
- handler({
15717
- method: "GET",
15718
- url: params.response.url,
15719
- status: params.response.status,
15720
- duration
15721
- });
15722
- };
15723
- this.on("Network.requestWillBeSent", onRequest);
15724
- this.on("Network.responseReceived", onResponse);
15725
- return () => {
15726
- this.off("Network.requestWillBeSent", onRequest);
15727
- this.off("Network.responseReceived", onResponse);
15728
- };
15729
- }
15730
- async detach() {
15731
- try {
15732
- await this.session.detach();
15733
- } catch {}
15734
- }
15735
- }
15736
- var init_cdp = __esm(() => {
15737
- init_types();
15738
- });
15739
-
15740
16133
  // src/lib/performance.ts
15741
16134
  async function getPerformanceMetrics(page) {
15742
16135
  const navTiming = await page.evaluate(() => {
@@ -15842,16 +16235,16 @@ __export(exports_downloads, {
15842
16235
  cleanStaleDownloads: () => cleanStaleDownloads
15843
16236
  });
15844
16237
  import { randomUUID as randomUUID9 } from "crypto";
15845
- import { join as join4, basename, extname } from "path";
15846
- import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
15847
- import { homedir as homedir4 } from "os";
16238
+ import { join as join5, basename, extname } from "path";
16239
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
16240
+ import { homedir as homedir5 } from "os";
15848
16241
  function getDataDir3() {
15849
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
16242
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15850
16243
  }
15851
16244
  function getDownloadsDir(sessionId) {
15852
- const base = join4(getDataDir3(), "downloads");
15853
- const dir = sessionId ? join4(base, sessionId) : base;
15854
- mkdirSync4(dir, { recursive: true });
16245
+ const base = join5(getDataDir3(), "downloads");
16246
+ const dir = sessionId ? join5(base, sessionId) : base;
16247
+ mkdirSync5(dir, { recursive: true });
15855
16248
  return dir;
15856
16249
  }
15857
16250
  function ensureDownloadsDir() {
@@ -15866,7 +16259,7 @@ function saveToDownloads(buffer, filename, opts) {
15866
16259
  const ext = extname(filename) || "";
15867
16260
  const stem = basename(filename, ext);
15868
16261
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
15869
- const filePath = join4(dir, uniqueName);
16262
+ const filePath = join5(dir, uniqueName);
15870
16263
  writeFileSync(filePath, buffer);
15871
16264
  const meta = {
15872
16265
  id,
@@ -15895,20 +16288,20 @@ function listDownloads(sessionId) {
15895
16288
  const dir = getDownloadsDir(sessionId);
15896
16289
  const results = [];
15897
16290
  function scanDir(d) {
15898
- if (!existsSync(d))
16291
+ if (!existsSync2(d))
15899
16292
  return;
15900
- const entries = readdirSync(d);
16293
+ const entries = readdirSync2(d);
15901
16294
  for (const entry of entries) {
15902
16295
  if (entry.endsWith(".meta.json"))
15903
16296
  continue;
15904
- const full = join4(d, entry);
16297
+ const full = join5(d, entry);
15905
16298
  const stat = statSync(full);
15906
16299
  if (stat.isDirectory()) {
15907
16300
  scanDir(full);
15908
16301
  continue;
15909
16302
  }
15910
16303
  const mpath = metaPath(full);
15911
- if (!existsSync(mpath))
16304
+ if (!existsSync2(mpath))
15912
16305
  continue;
15913
16306
  try {
15914
16307
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -15938,9 +16331,9 @@ function deleteDownload(id, sessionId) {
15938
16331
  if (!file)
15939
16332
  return false;
15940
16333
  try {
15941
- unlinkSync(file.path);
15942
- if (existsSync(file.meta_path))
15943
- unlinkSync(file.meta_path);
16334
+ unlinkSync2(file.path);
16335
+ if (existsSync2(file.meta_path))
16336
+ unlinkSync2(file.meta_path);
15944
16337
  return true;
15945
16338
  } catch {
15946
16339
  return false;
@@ -15990,9 +16383,9 @@ var exports_gallery_diff = {};
15990
16383
  __export(exports_gallery_diff, {
15991
16384
  diffImages: () => diffImages
15992
16385
  });
15993
- import { join as join5 } from "path";
15994
- import { mkdirSync as mkdirSync5 } from "fs";
15995
- import { homedir as homedir5 } from "os";
16386
+ import { join as join6 } from "path";
16387
+ import { mkdirSync as mkdirSync6 } from "fs";
16388
+ import { homedir as homedir6 } from "os";
15996
16389
  async function diffImages(path1, path2) {
15997
16390
  const img1 = import_sharp2.default(path1);
15998
16391
  const img2 = import_sharp2.default(path2);
@@ -16023,10 +16416,10 @@ async function diffImages(path1, path2) {
16023
16416
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
16024
16417
  }
16025
16418
  }
16026
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
16027
- const diffDir = join5(dataDir, "diffs");
16028
- mkdirSync5(diffDir, { recursive: true });
16029
- const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
16419
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16420
+ const diffDir = join6(dataDir, "diffs");
16421
+ mkdirSync6(diffDir, { recursive: true });
16422
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
16030
16423
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
16031
16424
  await Bun.write(diffPath, diffImageBuffer);
16032
16425
  return {
@@ -16043,9 +16436,9 @@ var init_gallery_diff = __esm(() => {
16043
16436
  });
16044
16437
 
16045
16438
  // src/lib/files-integration.ts
16046
- import { join as join6 } from "path";
16047
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
16048
- import { homedir as homedir6 } from "os";
16439
+ import { join as join7 } from "path";
16440
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
16441
+ import { homedir as homedir7 } from "os";
16049
16442
  async function persistFile(localPath, opts) {
16050
16443
  try {
16051
16444
  const mod = await import("@hasna/files");
@@ -16054,12 +16447,12 @@ async function persistFile(localPath, opts) {
16054
16447
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
16055
16448
  }
16056
16449
  } catch {}
16057
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16450
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
16058
16451
  const date = new Date().toISOString().split("T")[0];
16059
- const dir = join6(dataDir, "persistent", date);
16060
- mkdirSync6(dir, { recursive: true });
16452
+ const dir = join7(dataDir, "persistent", date);
16453
+ mkdirSync7(dir, { recursive: true });
16061
16454
  const filename = localPath.split("/").pop() ?? "file";
16062
- const targetPath = join6(dir, filename);
16455
+ const targetPath = join7(dir, filename);
16063
16456
  copyFileSync2(localPath, targetPath);
16064
16457
  return {
16065
16458
  id: `local-${Date.now()}`,
@@ -16174,23 +16567,23 @@ __export(exports_profiles, {
16174
16567
  deleteProfile: () => deleteProfile,
16175
16568
  applyProfile: () => applyProfile
16176
16569
  });
16177
- import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16178
- import { join as join7 } from "path";
16179
- import { homedir as homedir7 } from "os";
16570
+ import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16571
+ import { join as join8 } from "path";
16572
+ import { homedir as homedir8 } from "os";
16180
16573
  function getProfilesDir() {
16181
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
16182
- const dir = join7(dataDir, "profiles");
16183
- mkdirSync7(dir, { recursive: true });
16574
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
16575
+ const dir = join8(dataDir, "profiles");
16576
+ mkdirSync8(dir, { recursive: true });
16184
16577
  return dir;
16185
16578
  }
16186
16579
  function getProfileDir2(name) {
16187
- return join7(getProfilesDir(), name);
16580
+ return join8(getProfilesDir(), name);
16188
16581
  }
16189
16582
  async function saveProfile(page, name) {
16190
16583
  const dir = getProfileDir2(name);
16191
- mkdirSync7(dir, { recursive: true });
16584
+ mkdirSync8(dir, { recursive: true });
16192
16585
  const cookies = await page.context().cookies();
16193
- writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16586
+ writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16194
16587
  let localStorage2 = {};
16195
16588
  try {
16196
16589
  localStorage2 = await page.evaluate(() => {
@@ -16202,11 +16595,11 @@ async function saveProfile(page, name) {
16202
16595
  return result;
16203
16596
  });
16204
16597
  } catch {}
16205
- writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16598
+ writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16206
16599
  const savedAt = new Date().toISOString();
16207
16600
  const url = page.url();
16208
16601
  const meta = { saved_at: savedAt, url };
16209
- writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
16602
+ writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
16210
16603
  return {
16211
16604
  name,
16212
16605
  saved_at: savedAt,
@@ -16217,17 +16610,17 @@ async function saveProfile(page, name) {
16217
16610
  }
16218
16611
  function loadProfile(name) {
16219
16612
  const dir = getProfileDir2(name);
16220
- if (!existsSync3(dir)) {
16613
+ if (!existsSync4(dir)) {
16221
16614
  throw new Error(`Profile not found: ${name}`);
16222
16615
  }
16223
- const cookiesPath = join7(dir, "cookies.json");
16224
- const storagePath = join7(dir, "storage.json");
16225
- const metaPath2 = join7(dir, "meta.json");
16226
- const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
16227
- const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
16616
+ const cookiesPath = join8(dir, "cookies.json");
16617
+ const storagePath = join8(dir, "storage.json");
16618
+ const metaPath2 = join8(dir, "meta.json");
16619
+ const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
16620
+ const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
16228
16621
  let savedAt = new Date().toISOString();
16229
16622
  let url;
16230
- if (existsSync3(metaPath2)) {
16623
+ if (existsSync4(metaPath2)) {
16231
16624
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16232
16625
  savedAt = meta.saved_at ?? savedAt;
16233
16626
  url = meta.url;
@@ -16255,33 +16648,33 @@ async function applyProfile(page, profileData) {
16255
16648
  }
16256
16649
  function listProfiles() {
16257
16650
  const dir = getProfilesDir();
16258
- if (!existsSync3(dir))
16651
+ if (!existsSync4(dir))
16259
16652
  return [];
16260
- const entries = readdirSync2(dir, { withFileTypes: true });
16653
+ const entries = readdirSync3(dir, { withFileTypes: true });
16261
16654
  const profiles = [];
16262
16655
  for (const entry of entries) {
16263
16656
  if (!entry.isDirectory())
16264
16657
  continue;
16265
16658
  const name = entry.name;
16266
- const profileDir = join7(dir, name);
16659
+ const profileDir = join8(dir, name);
16267
16660
  let savedAt = "";
16268
16661
  let url;
16269
16662
  let cookieCount = 0;
16270
16663
  let storageKeyCount = 0;
16271
16664
  try {
16272
- const metaPath2 = join7(profileDir, "meta.json");
16273
- if (existsSync3(metaPath2)) {
16665
+ const metaPath2 = join8(profileDir, "meta.json");
16666
+ if (existsSync4(metaPath2)) {
16274
16667
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16275
16668
  savedAt = meta.saved_at ?? "";
16276
16669
  url = meta.url;
16277
16670
  }
16278
- const cookiesPath = join7(profileDir, "cookies.json");
16279
- if (existsSync3(cookiesPath)) {
16671
+ const cookiesPath = join8(profileDir, "cookies.json");
16672
+ if (existsSync4(cookiesPath)) {
16280
16673
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
16281
16674
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
16282
16675
  }
16283
- const storagePath = join7(profileDir, "storage.json");
16284
- if (existsSync3(storagePath)) {
16676
+ const storagePath = join8(profileDir, "storage.json");
16677
+ if (existsSync4(storagePath)) {
16285
16678
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
16286
16679
  storageKeyCount = Object.keys(storage).length;
16287
16680
  }
@@ -16298,7 +16691,7 @@ function listProfiles() {
16298
16691
  }
16299
16692
  function deleteProfile(name) {
16300
16693
  const dir = getProfileDir2(name);
16301
- if (!existsSync3(dir))
16694
+ if (!existsSync4(dir))
16302
16695
  return false;
16303
16696
  try {
16304
16697
  rmSync(dir, { recursive: true, force: true });
@@ -16309,6 +16702,97 @@ function deleteProfile(name) {
16309
16702
  }
16310
16703
  var init_profiles = () => {};
16311
16704
 
16705
+ // src/lib/sanitize.ts
16706
+ var exports_sanitize = {};
16707
+ __export(exports_sanitize, {
16708
+ sanitizeText: () => sanitizeText,
16709
+ sanitizeHTML: () => sanitizeHTML
16710
+ });
16711
+ function sanitizeText(text) {
16712
+ let stripped = 0;
16713
+ const warnings = [];
16714
+ let clean = text;
16715
+ for (const pattern of INJECTION_PATTERNS) {
16716
+ pattern.lastIndex = 0;
16717
+ const matches = clean.match(pattern);
16718
+ if (matches) {
16719
+ stripped += matches.length;
16720
+ warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
16721
+ pattern.lastIndex = 0;
16722
+ clean = clean.replace(pattern, "[STRIPPED]");
16723
+ }
16724
+ }
16725
+ return { text: clean, stripped, warnings };
16726
+ }
16727
+ function sanitizeHTML(html) {
16728
+ let stripped = 0;
16729
+ const warnings = [];
16730
+ let clean = html;
16731
+ const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
16732
+ if (commentMatches) {
16733
+ for (const comment of commentMatches) {
16734
+ if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
16735
+ stripped++;
16736
+ warnings.push(`Stripped HTML comment (${comment.length} chars)`);
16737
+ }
16738
+ }
16739
+ clean = clean.replace(/<!--[\s\S]*?-->/g, "");
16740
+ }
16741
+ const hiddenPatterns = [
16742
+ /style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
16743
+ /style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
16744
+ /style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
16745
+ /style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
16746
+ /style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
16747
+ ];
16748
+ for (const pattern of hiddenPatterns) {
16749
+ pattern.lastIndex = 0;
16750
+ const matches = clean.match(pattern);
16751
+ if (matches) {
16752
+ stripped += matches.length;
16753
+ warnings.push(`Stripped ${matches.length} hidden elements`);
16754
+ pattern.lastIndex = 0;
16755
+ clean = clean.replace(pattern, "");
16756
+ }
16757
+ }
16758
+ const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
16759
+ const ariaHidden = clean.match(ariaHiddenPattern);
16760
+ if (ariaHidden) {
16761
+ stripped += ariaHidden.length;
16762
+ warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
16763
+ ariaHiddenPattern.lastIndex = 0;
16764
+ clean = clean.replace(ariaHiddenPattern, "");
16765
+ }
16766
+ const textResult = sanitizeText(clean);
16767
+ return {
16768
+ text: textResult.text,
16769
+ stripped: stripped + textResult.stripped,
16770
+ warnings: [...warnings, ...textResult.warnings]
16771
+ };
16772
+ }
16773
+ var INJECTION_PATTERNS;
16774
+ var init_sanitize = __esm(() => {
16775
+ INJECTION_PATTERNS = [
16776
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
16777
+ /ignore\s+(all\s+)?prior\s+instructions/gi,
16778
+ /disregard\s+(all\s+)?previous/gi,
16779
+ /forget\s+(all\s+)?previous/gi,
16780
+ /you\s+are\s+now\s+/gi,
16781
+ /new\s+instructions?\s*:/gi,
16782
+ /system\s+prompt\s*:/gi,
16783
+ /\[INST\]/gi,
16784
+ /\[\/INST\]/gi,
16785
+ /<\|im_start\|>/gi,
16786
+ /<\|im_end\|>/gi,
16787
+ /<<SYS>>/gi,
16788
+ /<<\/SYS>>/gi,
16789
+ /IMPORTANT:\s*ignore/gi,
16790
+ /CRITICAL:\s*override/gi,
16791
+ /assistant:\s/gi,
16792
+ /human:\s/gi
16793
+ ];
16794
+ });
16795
+
16312
16796
  // src/lib/annotate.ts
16313
16797
  var exports_annotate = {};
16314
16798
  __export(exports_annotate, {
@@ -16369,26 +16853,846 @@ var init_annotate = __esm(() => {
16369
16853
  import_sharp3 = __toESM(require_lib(), 1);
16370
16854
  });
16371
16855
 
16856
+ // src/lib/env-detector.ts
16857
+ var exports_env_detector = {};
16858
+ __export(exports_env_detector, {
16859
+ detectEnvironment: () => detectEnvironment
16860
+ });
16861
+ async function detectEnvironment(page) {
16862
+ const url = page.url();
16863
+ const signals = [];
16864
+ let score = { local: 0, dev: 0, staging: 0, prod: 0 };
16865
+ try {
16866
+ const u = new URL(url);
16867
+ if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "0.0.0.0" || u.hostname.endsWith(".local")) {
16868
+ score.local += 5;
16869
+ signals.push(`URL hostname: ${u.hostname} \u2192 local`);
16870
+ } else if (u.hostname.match(/^(dev|development)\./i) || u.port !== "") {
16871
+ score.dev += 4;
16872
+ signals.push(`URL pattern: ${u.hostname}:${u.port} \u2192 dev`);
16873
+ } else if (u.hostname.match(/^(staging|stg|stage|preprod|uat)\./i)) {
16874
+ score.staging += 4;
16875
+ signals.push(`URL pattern: ${u.hostname} \u2192 staging`);
16876
+ } else {
16877
+ score.prod += 2;
16878
+ signals.push(`URL looks production: ${u.hostname}`);
16879
+ }
16880
+ if (u.port && !["80", "443", ""].includes(u.port)) {
16881
+ score.dev += 2;
16882
+ signals.push(`Non-standard port: ${u.port}`);
16883
+ }
16884
+ if (u.protocol === "https:") {
16885
+ score.prod += 1;
16886
+ signals.push("HTTPS \u2192 likely prod");
16887
+ } else {
16888
+ score.dev += 2;
16889
+ signals.push("HTTP \u2192 likely dev/local");
16890
+ }
16891
+ } catch {}
16892
+ try {
16893
+ const pageSignals = await page.evaluate(() => {
16894
+ const s = [];
16895
+ const w = window;
16896
+ const envVars = ["__ENV__", "__NEXT_DATA__", "__NUXT__", "process"];
16897
+ for (const v of envVars) {
16898
+ if (w[v]) {
16899
+ const env2 = w[v]?.env?.NODE_ENV ?? w[v]?.runtimeConfig?.public?.env ?? w[v]?.props?.pageProps?.env;
16900
+ if (env2)
16901
+ s.push(`window.${v}: ${env2}`);
16902
+ }
16903
+ }
16904
+ if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
16905
+ const fiber = document.querySelector("[data-reactroot]") || document.getElementById("__next") || document.getElementById("root");
16906
+ if (fiber)
16907
+ s.push("React app detected");
16908
+ }
16909
+ const envMeta = document.querySelector('meta[name="environment"], meta[name="env"], meta[name="deploy-env"]');
16910
+ if (envMeta)
16911
+ s.push(`meta[environment]: ${envMeta.getAttribute("content")}`);
16912
+ const scripts = document.querySelectorAll("script[src]");
16913
+ let minified = 0, unminified = 0;
16914
+ scripts.forEach((s2) => {
16915
+ const src = s2.getAttribute("src") ?? "";
16916
+ if (src.includes(".min.") || src.match(/\.[a-f0-9]{8,}\./))
16917
+ minified++;
16918
+ else if (src.endsWith(".js") && !src.includes("chunk"))
16919
+ unminified++;
16920
+ });
16921
+ if (unminified > minified && unminified > 2)
16922
+ s.push(`Unminified scripts (${unminified}/${minified + unminified}) \u2192 likely dev`);
16923
+ else if (minified > 0)
16924
+ s.push(`Minified/hashed scripts (${minified}/${minified + unminified}) \u2192 likely prod`);
16925
+ if (document.querySelector("[data-testid]"))
16926
+ s.push("data-testid attributes present \u2192 dev/staging");
16927
+ if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
16928
+ s.push("Service worker active \u2192 likely prod");
16929
+ }
16930
+ if (w.Sentry)
16931
+ s.push("Sentry SDK loaded \u2192 prod monitoring");
16932
+ if (w.__DATADOG_SYNTHETICS_INLINED_SCRIPT)
16933
+ s.push("Datadog loaded \u2192 prod monitoring");
16934
+ if (w.LogRocket)
16935
+ s.push("LogRocket loaded \u2192 prod monitoring");
16936
+ if (w._lr_loaded)
16937
+ s.push("LogRocket loaded \u2192 prod monitoring");
16938
+ if (w.gtag || w.ga)
16939
+ s.push("Google Analytics loaded \u2192 likely prod");
16940
+ if (w.posthog || w._ph)
16941
+ s.push("PostHog loaded \u2192 prod analytics");
16942
+ if (w.mixpanel)
16943
+ s.push("Mixpanel loaded \u2192 prod analytics");
16944
+ const robots = document.querySelector('meta[name="robots"]');
16945
+ if (robots) {
16946
+ const content = robots.getAttribute("content") ?? "";
16947
+ if (content.includes("noindex"))
16948
+ s.push(`robots: noindex \u2192 staging/dev`);
16949
+ }
16950
+ return s;
16951
+ });
16952
+ for (const signal of pageSignals) {
16953
+ signals.push(signal);
16954
+ if (signal.includes("development") || signal.includes("\u2192 dev") || signal.includes("\u2192 likely dev"))
16955
+ score.dev += 2;
16956
+ if (signal.includes("production") || signal.includes("\u2192 prod") || signal.includes("\u2192 likely prod"))
16957
+ score.prod += 2;
16958
+ if (signal.includes("staging") || signal.includes("\u2192 staging"))
16959
+ score.staging += 2;
16960
+ if (signal.includes("monitoring") || signal.includes("analytics"))
16961
+ score.prod += 1;
16962
+ if (signal.includes("noindex")) {
16963
+ score.staging += 2;
16964
+ score.dev += 1;
16965
+ }
16966
+ }
16967
+ } catch {}
16968
+ const entries = Object.entries(score);
16969
+ entries.sort((a, b) => b[1] - a[1]);
16970
+ const [env, topScore] = entries[0];
16971
+ const [, secondScore] = entries[1];
16972
+ const confidence = topScore >= 5 ? "high" : topScore > secondScore + 1 ? "medium" : "low";
16973
+ return { env, confidence, signals };
16974
+ }
16975
+
16976
+ // src/lib/deep-performance.ts
16977
+ var exports_deep_performance = {};
16978
+ __export(exports_deep_performance, {
16979
+ getDeepPerformance: () => getDeepPerformance
16980
+ });
16981
+ function categorizeThirdParty(domain) {
16982
+ for (const [pattern, category] of Object.entries(THIRD_PARTY_CATEGORIES)) {
16983
+ if (domain.includes(pattern))
16984
+ return category;
16985
+ }
16986
+ return "other";
16987
+ }
16988
+ async function getDeepPerformance(page) {
16989
+ return page.evaluate(() => {
16990
+ const perf = performance;
16991
+ const entries = perf.getEntriesByType("resource");
16992
+ const navEntry = perf.getEntriesByType("navigation")[0];
16993
+ const paintEntries = perf.getEntriesByType("paint");
16994
+ const fcp = paintEntries.find((e) => e.name === "first-contentful-paint")?.startTime;
16995
+ const ttfb = navEntry?.responseStart;
16996
+ const web_vitals = { fcp, ttfb };
16997
+ try {
16998
+ const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
16999
+ if (lcpEntries.length > 0)
17000
+ web_vitals.lcp = lcpEntries[lcpEntries.length - 1].startTime;
17001
+ } catch {}
17002
+ const byType = {};
17003
+ let totalBytes = 0;
17004
+ const resourceList = [];
17005
+ const pageDomain = location.hostname;
17006
+ const thirdPartyMap = new Map;
17007
+ for (const entry of entries) {
17008
+ const size = entry.transferSize || entry.encodedBodySize || 0;
17009
+ totalBytes += size;
17010
+ let type2 = entry.initiatorType || "other";
17011
+ if (type2 === "xmlhttprequest" || type2 === "fetch")
17012
+ type2 = "xhr";
17013
+ if (type2 === "link" && entry.name.match(/\.css/))
17014
+ type2 = "css";
17015
+ if (type2 === "img" || entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp|avif|ico)/i))
17016
+ type2 = "image";
17017
+ if (type2 === "script" || entry.name.match(/\.js/))
17018
+ type2 = "script";
17019
+ if (entry.name.match(/\.(woff2?|ttf|otf|eot)/i))
17020
+ type2 = "font";
17021
+ if (!byType[type2])
17022
+ byType[type2] = { count: 0, size_bytes: 0 };
17023
+ byType[type2].count++;
17024
+ byType[type2].size_bytes += size;
17025
+ resourceList.push({ url: entry.name, size_bytes: size, type: type2 });
17026
+ try {
17027
+ const domain = new URL(entry.name).hostname;
17028
+ if (domain !== pageDomain && !domain.endsWith(`.${pageDomain}`)) {
17029
+ if (!thirdPartyMap.has(domain))
17030
+ thirdPartyMap.set(domain, { scripts: 0, total_bytes: 0 });
17031
+ const tp = thirdPartyMap.get(domain);
17032
+ tp.scripts++;
17033
+ tp.total_bytes += size;
17034
+ }
17035
+ } catch {}
17036
+ }
17037
+ resourceList.sort((a, b) => b.size_bytes - a.size_bytes);
17038
+ const largest = resourceList.slice(0, 10).map((r) => ({
17039
+ url: r.url.length > 120 ? r.url.slice(0, 117) + "..." : r.url,
17040
+ size_bytes: r.size_bytes,
17041
+ type: r.type
17042
+ }));
17043
+ 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);
17044
+ const allNodes = document.querySelectorAll("*");
17045
+ let maxDepth = 0;
17046
+ let textNodes = 0;
17047
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);
17048
+ let node = walker.currentNode;
17049
+ while (node) {
17050
+ if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
17051
+ textNodes++;
17052
+ let depth = 0;
17053
+ let parent = node.parentNode;
17054
+ while (parent) {
17055
+ depth++;
17056
+ parent = parent.parentNode;
17057
+ }
17058
+ if (depth > maxDepth)
17059
+ maxDepth = depth;
17060
+ node = walker.nextNode();
17061
+ }
17062
+ const mem = performance.memory;
17063
+ const memory = {
17064
+ js_heap_used_mb: mem ? Math.round(mem.usedJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
17065
+ js_heap_total_mb: mem ? Math.round(mem.totalJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
17066
+ js_heap_limit_mb: mem ? Math.round(mem.jsHeapSizeLimit / 1024 / 1024 * 100) / 100 : 0
17067
+ };
17068
+ return {
17069
+ web_vitals,
17070
+ resources: { total_transfer_bytes: totalBytes, total_resources: entries.length, by_type: byType, largest },
17071
+ third_party,
17072
+ dom: { node_count: document.all.length, max_depth: maxDepth, element_count: allNodes.length, text_node_count: textNodes },
17073
+ main_thread: { long_tasks: 0, total_blocking_ms: 0 },
17074
+ memory
17075
+ };
17076
+ }).then((result) => {
17077
+ for (const tp of result.third_party) {
17078
+ tp.category = categorizeThirdParty(tp.domain);
17079
+ }
17080
+ return result;
17081
+ });
17082
+ }
17083
+ var THIRD_PARTY_CATEGORIES;
17084
+ var init_deep_performance = __esm(() => {
17085
+ THIRD_PARTY_CATEGORIES = {
17086
+ "google-analytics.com": "analytics",
17087
+ "googletagmanager.com": "analytics",
17088
+ gtag: "analytics",
17089
+ "facebook.net": "social",
17090
+ "connect.facebook": "social",
17091
+ "stripe.com": "payment",
17092
+ "js.stripe.com": "payment",
17093
+ "sentry.io": "monitoring",
17094
+ "sentry-cdn": "monitoring",
17095
+ "posthog.com": "analytics",
17096
+ "ph.": "analytics",
17097
+ "intercom.io": "chat",
17098
+ "crisp.chat": "chat",
17099
+ "hotjar.com": "analytics",
17100
+ "clarity.ms": "analytics",
17101
+ "cdn.jsdelivr.net": "cdn",
17102
+ "cdnjs.cloudflare.com": "cdn",
17103
+ "unpkg.com": "cdn",
17104
+ "fonts.googleapis.com": "fonts",
17105
+ "fonts.gstatic.com": "fonts"
17106
+ };
17107
+ });
17108
+
17109
+ // src/lib/workflows.ts
17110
+ var exports_workflows = {};
17111
+ __export(exports_workflows, {
17112
+ saveWorkflowFromRecording: () => saveWorkflowFromRecording,
17113
+ saveWorkflow: () => saveWorkflow,
17114
+ runWorkflow: () => runWorkflow,
17115
+ listWorkflows: () => listWorkflows,
17116
+ getWorkflowByName: () => getWorkflowByName,
17117
+ getWorkflow: () => getWorkflow,
17118
+ deleteWorkflow: () => deleteWorkflow
17119
+ });
17120
+ import { randomUUID as randomUUID10 } from "crypto";
17121
+ function saveWorkflow(data) {
17122
+ const db = getDatabase();
17123
+ const id = randomUUID10();
17124
+ 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);
17125
+ return getWorkflow(id);
17126
+ }
17127
+ function saveWorkflowFromRecording(recordingId, name, description) {
17128
+ const db = getDatabase();
17129
+ const rec = db.query("SELECT steps, start_url FROM recordings WHERE id = ?").get(recordingId);
17130
+ if (!rec)
17131
+ throw new Error(`Recording not found: ${recordingId}`);
17132
+ const steps = JSON.parse(rec.steps);
17133
+ return saveWorkflow({ name, description, steps, startUrl: rec.start_url ?? undefined });
17134
+ }
17135
+ function getWorkflow(id) {
17136
+ const db = getDatabase();
17137
+ const row = db.query("SELECT * FROM workflows WHERE id = ?").get(id);
17138
+ if (!row)
17139
+ return null;
17140
+ return { ...row, steps: JSON.parse(row.steps) };
17141
+ }
17142
+ function getWorkflowByName(name) {
17143
+ const db = getDatabase();
17144
+ const row = db.query("SELECT * FROM workflows WHERE name = ?").get(name);
17145
+ if (!row)
17146
+ return null;
17147
+ return { ...row, steps: JSON.parse(row.steps) };
17148
+ }
17149
+ function listWorkflows() {
17150
+ const db = getDatabase();
17151
+ return db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all().map((row) => ({ ...row, steps: JSON.parse(row.steps) }));
17152
+ }
17153
+ function deleteWorkflow(name) {
17154
+ const db = getDatabase();
17155
+ return db.prepare("DELETE FROM workflows WHERE name = ?").run(name).changes > 0;
17156
+ }
17157
+ function recordRun(id, healed) {
17158
+ const db = getDatabase();
17159
+ if (healed) {
17160
+ 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);
17161
+ } else {
17162
+ db.prepare("UPDATE workflows SET last_run = datetime('now'), run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
17163
+ }
17164
+ }
17165
+ async function runWorkflow(workflow, page) {
17166
+ const t0 = Date.now();
17167
+ let executed = 0;
17168
+ let failed = 0;
17169
+ let healed = 0;
17170
+ const healedDetails = [];
17171
+ const errors2 = [];
17172
+ const updatedSteps = [...workflow.steps];
17173
+ for (let i = 0;i < workflow.steps.length; i++) {
17174
+ const step = workflow.steps[i];
17175
+ try {
17176
+ switch (step.type) {
17177
+ case "navigate":
17178
+ if (step.url)
17179
+ await page.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
17180
+ break;
17181
+ case "click":
17182
+ if (step.selector) {
17183
+ try {
17184
+ await page.click(step.selector, { timeout: 5000 });
17185
+ } catch {
17186
+ const result = await healSelector(page, step.selector);
17187
+ if (result.found && result.locator) {
17188
+ await result.locator.click();
17189
+ healed++;
17190
+ const healedSelector = `[healed:${result.method}]${step.selector}`;
17191
+ healedDetails.push({ step: i, original: step.selector, healed_to: healedSelector, method: result.method });
17192
+ } else {
17193
+ throw new Error(`Click failed: ${step.selector} (self-healing exhausted)`);
17194
+ }
17195
+ }
17196
+ }
17197
+ break;
17198
+ case "type":
17199
+ if (step.selector && step.value) {
17200
+ try {
17201
+ await page.fill(step.selector, step.value);
17202
+ } catch {
17203
+ const result = await healSelector(page, step.selector);
17204
+ if (result.found && result.locator) {
17205
+ await result.locator.fill(step.value);
17206
+ healed++;
17207
+ healedDetails.push({ step: i, original: step.selector, healed_to: `[healed:${result.method}]`, method: result.method });
17208
+ } else {
17209
+ throw new Error(`Type failed: ${step.selector} (self-healing exhausted)`);
17210
+ }
17211
+ }
17212
+ }
17213
+ break;
17214
+ case "scroll":
17215
+ if (step.y)
17216
+ await page.mouse.wheel(0, step.y);
17217
+ break;
17218
+ case "hover":
17219
+ if (step.selector) {
17220
+ try {
17221
+ await page.hover(step.selector);
17222
+ } catch {}
17223
+ }
17224
+ break;
17225
+ case "select":
17226
+ if (step.selector && step.value) {
17227
+ try {
17228
+ await page.selectOption(step.selector, step.value);
17229
+ } catch {}
17230
+ }
17231
+ break;
17232
+ case "wait":
17233
+ if (step.selector) {
17234
+ try {
17235
+ await page.waitForSelector(step.selector, { timeout: 1e4 });
17236
+ } catch {}
17237
+ } else {
17238
+ await new Promise((r) => setTimeout(r, step.timestamp || 1000));
17239
+ }
17240
+ break;
17241
+ case "evaluate":
17242
+ if (step.value)
17243
+ await page.evaluate(step.value);
17244
+ break;
17245
+ default:
17246
+ break;
17247
+ }
17248
+ executed++;
17249
+ } catch (err) {
17250
+ failed++;
17251
+ errors2.push(`Step ${i} (${step.type}): ${err instanceof Error ? err.message : String(err)}`);
17252
+ }
17253
+ }
17254
+ recordRun(workflow.id, healed > 0);
17255
+ return {
17256
+ success: failed === 0,
17257
+ steps_executed: executed,
17258
+ steps_failed: failed,
17259
+ steps_healed: healed,
17260
+ healed_details: healedDetails,
17261
+ errors: errors2,
17262
+ duration_ms: Date.now() - t0
17263
+ };
17264
+ }
17265
+ var init_workflows = __esm(() => {
17266
+ init_schema();
17267
+ });
17268
+
17269
+ // src/lib/auth-flow.ts
17270
+ var exports_auth_flow = {};
17271
+ __export(exports_auth_flow, {
17272
+ tryReplayAuth: () => tryReplayAuth,
17273
+ touchAuthFlow: () => touchAuthFlow,
17274
+ saveAuthFlow: () => saveAuthFlow,
17275
+ listAuthFlows: () => listAuthFlows,
17276
+ isAuthRedirect: () => isAuthRedirect,
17277
+ isAuthPage: () => isAuthPage,
17278
+ getAuthFlowByName: () => getAuthFlowByName,
17279
+ getAuthFlowByDomain: () => getAuthFlowByDomain,
17280
+ getAuthFlow: () => getAuthFlow,
17281
+ deleteAuthFlow: () => deleteAuthFlow
17282
+ });
17283
+ import { randomUUID as randomUUID11 } from "crypto";
17284
+ function saveAuthFlow(data) {
17285
+ const db = getDatabase();
17286
+ const id = randomUUID11();
17287
+ 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);
17288
+ return getAuthFlow(id);
17289
+ }
17290
+ function getAuthFlow(id) {
17291
+ const db = getDatabase();
17292
+ return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
17293
+ }
17294
+ function getAuthFlowByName(name) {
17295
+ const db = getDatabase();
17296
+ return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
17297
+ }
17298
+ function getAuthFlowByDomain(domain) {
17299
+ const db = getDatabase();
17300
+ return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
17301
+ }
17302
+ function listAuthFlows() {
17303
+ const db = getDatabase();
17304
+ return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
17305
+ }
17306
+ function deleteAuthFlow(name) {
17307
+ const db = getDatabase();
17308
+ const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
17309
+ return result.changes > 0;
17310
+ }
17311
+ function touchAuthFlow(id) {
17312
+ const db = getDatabase();
17313
+ db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
17314
+ }
17315
+ function isAuthPage(url) {
17316
+ return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
17317
+ }
17318
+ function isAuthRedirect(fromUrl, toUrl) {
17319
+ if (fromUrl === toUrl)
17320
+ return false;
17321
+ return isAuthPage(toUrl) && !isAuthPage(fromUrl);
17322
+ }
17323
+ async function tryReplayAuth(page, domain) {
17324
+ const flow = getAuthFlowByDomain(domain);
17325
+ if (!flow)
17326
+ return { replayed: false };
17327
+ if (flow.storage_state_path) {
17328
+ try {
17329
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
17330
+ if (existsSync5(flow.storage_state_path)) {
17331
+ const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
17332
+ if (state.cookies?.length) {
17333
+ await page.context().addCookies(state.cookies);
17334
+ await page.reload();
17335
+ await new Promise((r) => setTimeout(r, 1000));
17336
+ if (!isAuthPage(page.url())) {
17337
+ touchAuthFlow(flow.id);
17338
+ return { replayed: true, flow, method: "storage_state" };
17339
+ }
17340
+ }
17341
+ }
17342
+ } catch {}
17343
+ }
17344
+ if (flow.recording_id) {
17345
+ try {
17346
+ const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
17347
+ const result = await replayRecording2(flow.recording_id, page);
17348
+ if (result.success) {
17349
+ try {
17350
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
17351
+ const path = await saveStateFromPage2(page, flow.name);
17352
+ const db = getDatabase();
17353
+ db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
17354
+ } catch {}
17355
+ touchAuthFlow(flow.id);
17356
+ return { replayed: true, flow, method: "recording_replay" };
17357
+ }
17358
+ } catch {}
17359
+ }
17360
+ return { replayed: false, flow };
17361
+ }
17362
+ var AUTH_URL_PATTERNS;
17363
+ var init_auth_flow = __esm(() => {
17364
+ init_schema();
17365
+ AUTH_URL_PATTERNS = [
17366
+ /\/login/i,
17367
+ /\/signin/i,
17368
+ /\/sign-in/i,
17369
+ /\/auth/i,
17370
+ /\/sso/i,
17371
+ /\/oauth/i,
17372
+ /\/cas\/login/i,
17373
+ /accounts\.google\.com/i,
17374
+ /login\.microsoftonline\.com/i,
17375
+ /github\.com\/login/i,
17376
+ /auth0\.com/i
17377
+ ];
17378
+ });
17379
+
17380
+ // src/lib/vision-fallback.ts
17381
+ var exports_vision_fallback = {};
17382
+ __export(exports_vision_fallback, {
17383
+ findElementByVision: () => findElementByVision,
17384
+ clickByVision: () => clickByVision
17385
+ });
17386
+ async function findElementByVision(page, description, opts) {
17387
+ const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
17388
+ const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
17389
+ const base64 = screenshot.toString("base64");
17390
+ const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
17391
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
17392
+ if (!apiKey) {
17393
+ return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
17394
+ }
17395
+ try {
17396
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
17397
+ method: "POST",
17398
+ headers: {
17399
+ "content-type": "application/json",
17400
+ "x-api-key": apiKey,
17401
+ "anthropic-version": "2023-06-01"
17402
+ },
17403
+ body: JSON.stringify({
17404
+ model,
17405
+ max_tokens: 256,
17406
+ messages: [{
17407
+ role: "user",
17408
+ content: [
17409
+ {
17410
+ type: "image",
17411
+ source: { type: "base64", media_type: "image/jpeg", data: base64 }
17412
+ },
17413
+ {
17414
+ type: "text",
17415
+ text: `Find the element matching this description: "${description}"
17416
+
17417
+ The screenshot is ${viewport.width}x${viewport.height} pixels.
17418
+
17419
+ Reply with ONLY a JSON object (no markdown, no explanation):
17420
+ {"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
17421
+
17422
+ If you cannot find the element:
17423
+ {"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
17424
+ }
17425
+ ]
17426
+ }]
17427
+ })
17428
+ });
17429
+ const data = await response.json();
17430
+ const text = data.content?.[0]?.text ?? "";
17431
+ const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
17432
+ const result = JSON.parse(jsonStr);
17433
+ result.model = model;
17434
+ return result;
17435
+ } catch (err) {
17436
+ return {
17437
+ found: false,
17438
+ x: 0,
17439
+ y: 0,
17440
+ confidence: "none",
17441
+ description,
17442
+ model,
17443
+ error: err instanceof Error ? err.message : String(err)
17444
+ };
17445
+ }
17446
+ }
17447
+ async function clickByVision(page, description, opts) {
17448
+ const result = await findElementByVision(page, description, opts);
17449
+ if (result.found && result.x > 0 && result.y > 0) {
17450
+ await page.mouse.click(result.x, result.y);
17451
+ }
17452
+ return result;
17453
+ }
17454
+ var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
17455
+
17456
+ // src/lib/api-detector.ts
17457
+ var exports_api_detector = {};
17458
+ __export(exports_api_detector, {
17459
+ listDiscoveredAPIs: () => listDiscoveredAPIs,
17460
+ detectAPIs: () => detectAPIs
17461
+ });
17462
+ import { randomUUID as randomUUID12 } from "crypto";
17463
+ function detectAPIs(sessionId) {
17464
+ const db = getDatabase();
17465
+ const requests = db.query(`SELECT method, url, status_code, response_headers, body_size
17466
+ FROM network_log
17467
+ WHERE session_id = ?
17468
+ AND (response_headers LIKE '%application/json%' OR response_headers LIKE '%text/json%')
17469
+ AND status_code >= 200 AND status_code < 400
17470
+ ORDER BY timestamp DESC`).all(sessionId);
17471
+ const seen = new Map;
17472
+ for (const req of requests) {
17473
+ try {
17474
+ const urlObj = new URL(req.url);
17475
+ if (urlObj.pathname.match(/\.(js|css|png|jpg|svg|woff|ico)$/))
17476
+ continue;
17477
+ if (urlObj.hostname.includes("googleapis.com/identitytoolkit"))
17478
+ continue;
17479
+ if (urlObj.hostname.includes("posthog"))
17480
+ continue;
17481
+ if (urlObj.hostname.includes("sentry"))
17482
+ continue;
17483
+ const key = `${req.method} ${urlObj.origin}${urlObj.pathname}`;
17484
+ if (!seen.has(key)) {
17485
+ seen.set(key, {
17486
+ url: `${urlObj.origin}${urlObj.pathname}`,
17487
+ method: req.method,
17488
+ status_code: req.status_code,
17489
+ content_type: "application/json",
17490
+ response_schema: {},
17491
+ sample_size: req.body_size ?? 0
17492
+ });
17493
+ }
17494
+ } catch {}
17495
+ }
17496
+ const apis = Array.from(seen.values());
17497
+ for (const api of apis) {
17498
+ const id = randomUUID12();
17499
+ 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);
17500
+ }
17501
+ return apis;
17502
+ }
17503
+ function listDiscoveredAPIs(sessionId) {
17504
+ const db = getDatabase();
17505
+ if (sessionId) {
17506
+ return db.query("SELECT * FROM api_endpoints WHERE session_id = ? ORDER BY discovered_at DESC").all(sessionId);
17507
+ }
17508
+ return db.query("SELECT * FROM api_endpoints ORDER BY discovered_at DESC LIMIT 100").all();
17509
+ }
17510
+ var init_api_detector = __esm(() => {
17511
+ init_schema();
17512
+ });
17513
+
17514
+ // src/lib/structured-extract.ts
17515
+ var exports_structured_extract = {};
17516
+ __export(exports_structured_extract, {
17517
+ extractStructuredData: () => extractStructuredData
17518
+ });
17519
+ async function extractStructuredData(page) {
17520
+ return page.evaluate(() => {
17521
+ const result = { tables: [], lists: [], jsonLd: [], openGraph: {}, metaTags: {}, repeatedElements: [] };
17522
+ document.querySelectorAll("table").forEach((table, idx) => {
17523
+ const headers = [];
17524
+ table.querySelectorAll("thead th, thead td, tr:first-child th").forEach((th) => {
17525
+ headers.push(th.textContent?.trim() ?? "");
17526
+ });
17527
+ const rows = [];
17528
+ table.querySelectorAll("tbody tr, tr:not(:first-child)").forEach((tr) => {
17529
+ const row = [];
17530
+ tr.querySelectorAll("td, th").forEach((td) => {
17531
+ row.push(td.textContent?.trim() ?? "");
17532
+ });
17533
+ if (row.length > 0 && row.some((c) => c !== ""))
17534
+ rows.push(row);
17535
+ });
17536
+ if (rows.length > 0) {
17537
+ result.tables.push({ headers, rows, selector: `table:nth-of-type(${idx + 1})` });
17538
+ }
17539
+ });
17540
+ document.querySelectorAll("ul, ol").forEach((list, idx) => {
17541
+ const items = [];
17542
+ list.querySelectorAll(":scope > li").forEach((li) => {
17543
+ const text = li.textContent?.trim() ?? "";
17544
+ if (text)
17545
+ items.push(text);
17546
+ });
17547
+ if (items.length >= 3) {
17548
+ const tag = list.tagName.toLowerCase();
17549
+ result.lists.push({ items, selector: `${tag}:nth-of-type(${idx + 1})` });
17550
+ }
17551
+ });
17552
+ document.querySelectorAll('script[type="application/ld+json"]').forEach((script) => {
17553
+ try {
17554
+ result.jsonLd.push(JSON.parse(script.textContent ?? ""));
17555
+ } catch {}
17556
+ });
17557
+ document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
17558
+ const prop = meta.getAttribute("property")?.replace("og:", "") ?? "";
17559
+ result.openGraph[prop] = meta.getAttribute("content") ?? "";
17560
+ });
17561
+ document.querySelectorAll("meta[name]").forEach((meta) => {
17562
+ const name = meta.getAttribute("name") ?? "";
17563
+ if (name)
17564
+ result.metaTags[name] = meta.getAttribute("content") ?? "";
17565
+ });
17566
+ const classCounts = new Map;
17567
+ document.querySelectorAll("[class]").forEach((el) => {
17568
+ const cls = el.className.toString().trim();
17569
+ if (cls && cls.length > 5 && cls.length < 100) {
17570
+ if (!classCounts.has(cls))
17571
+ classCounts.set(cls, []);
17572
+ classCounts.get(cls).push(el);
17573
+ }
17574
+ });
17575
+ for (const [cls, elements] of classCounts) {
17576
+ if (elements.length >= 3 && elements.length <= 200) {
17577
+ const sample = elements.slice(0, 3).map((el) => el.textContent?.trim().slice(0, 100) ?? "");
17578
+ if (sample.some((s) => s.length > 10)) {
17579
+ result.repeatedElements.push({
17580
+ selector: `.${cls.split(" ")[0]}`,
17581
+ count: elements.length,
17582
+ sample
17583
+ });
17584
+ }
17585
+ }
17586
+ }
17587
+ result.repeatedElements.sort((a, b) => b.count - a.count);
17588
+ result.repeatedElements = result.repeatedElements.slice(0, 10);
17589
+ return result;
17590
+ });
17591
+ }
17592
+
17593
+ // src/lib/datasets.ts
17594
+ var exports_datasets = {};
17595
+ __export(exports_datasets, {
17596
+ saveDataset: () => saveDataset,
17597
+ listDatasets: () => listDatasets,
17598
+ getDatasetByName: () => getDatasetByName,
17599
+ getDataset: () => getDataset,
17600
+ exportDataset: () => exportDataset,
17601
+ deleteDataset: () => deleteDataset
17602
+ });
17603
+ import { randomUUID as randomUUID13 } from "crypto";
17604
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
17605
+ import { join as join9 } from "path";
17606
+ import { homedir as homedir9 } from "os";
17607
+ function saveDataset(data) {
17608
+ const db = getDatabase();
17609
+ const id = randomUUID13();
17610
+ const existing = db.query("SELECT id FROM datasets WHERE name = ?").get(data.name);
17611
+ if (existing) {
17612
+ 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);
17613
+ return getDataset(existing.id);
17614
+ }
17615
+ 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);
17616
+ return getDataset(id);
17617
+ }
17618
+ function getDataset(id) {
17619
+ const db = getDatabase();
17620
+ const row = db.query("SELECT * FROM datasets WHERE id = ?").get(id);
17621
+ if (!row)
17622
+ return null;
17623
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
17624
+ }
17625
+ function getDatasetByName(name) {
17626
+ const db = getDatabase();
17627
+ const row = db.query("SELECT * FROM datasets WHERE name = ?").get(name);
17628
+ if (!row)
17629
+ return null;
17630
+ return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
17631
+ }
17632
+ function listDatasets() {
17633
+ const db = getDatabase();
17634
+ 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 }));
17635
+ }
17636
+ function deleteDataset(name) {
17637
+ const db = getDatabase();
17638
+ return db.prepare("DELETE FROM datasets WHERE name = ?").run(name).changes > 0;
17639
+ }
17640
+ function exportDataset(name, format) {
17641
+ const dataset = getDatasetByName(name);
17642
+ if (!dataset)
17643
+ throw new Error(`Dataset '${name}' not found`);
17644
+ const dir = join9(process.env["BROWSER_DATA_DIR"] ?? join9(homedir9(), ".browser"), "exports");
17645
+ mkdirSync9(dir, { recursive: true });
17646
+ const filename = `${name}.${format}`;
17647
+ const path = join9(dir, filename);
17648
+ if (format === "csv") {
17649
+ const rows = dataset.data;
17650
+ if (rows.length === 0) {
17651
+ writeFileSync3(path, "");
17652
+ return { path, size: 0 };
17653
+ }
17654
+ const headers = Object.keys(rows[0]);
17655
+ const csvLines = [headers.join(",")];
17656
+ for (const row of rows) {
17657
+ csvLines.push(headers.map((h) => {
17658
+ const val = String(row[h] ?? "");
17659
+ return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
17660
+ }).join(","));
17661
+ }
17662
+ const content = csvLines.join(`
17663
+ `);
17664
+ writeFileSync3(path, content);
17665
+ return { path, size: content.length };
17666
+ } else {
17667
+ const content = JSON.stringify(dataset.data, null, 2);
17668
+ writeFileSync3(path, content);
17669
+ return { path, size: content.length };
17670
+ }
17671
+ }
17672
+ var init_datasets = __esm(() => {
17673
+ init_schema();
17674
+ });
17675
+
16372
17676
  // src/lib/auth.ts
16373
17677
  var exports_auth = {};
16374
17678
  __export(exports_auth, {
16375
17679
  loginWithCredentials: () => loginWithCredentials,
16376
17680
  getCredentials: () => getCredentials
16377
17681
  });
16378
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
16379
- import { join as join8 } from "path";
16380
- import { homedir as homedir8 } from "os";
17682
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
17683
+ import { join as join10 } from "path";
17684
+ import { homedir as homedir10 } from "os";
16381
17685
  async function getCredentials(service) {
16382
17686
  try {
16383
- const { getSecret } = await import(`${homedir8()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
17687
+ const { getSecret } = await import(`${homedir10()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
16384
17688
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
16385
17689
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
16386
17690
  if (email?.value && password?.value) {
16387
17691
  return { email: email.value, password: password.value };
16388
17692
  }
16389
17693
  } catch {}
16390
- const secretsPath = join8(homedir8(), ".secrets");
16391
- if (existsSync4(secretsPath)) {
17694
+ const secretsPath = join10(homedir10(), ".secrets");
17695
+ if (existsSync5(secretsPath)) {
16392
17696
  const content = readFileSync3(secretsPath, "utf8");
16393
17697
  const lines = content.split(`
16394
17698
  `);
@@ -16565,14 +17869,14 @@ __export(exports_dist, {
16565
17869
  InvalidScopeError: () => InvalidScopeError,
16566
17870
  EntityNotFoundError: () => EntityNotFoundError,
16567
17871
  DuplicateMemoryError: () => DuplicateMemoryError,
16568
- DEFAULT_MODEL: () => DEFAULT_MODEL,
17872
+ DEFAULT_MODEL: () => DEFAULT_MODEL2,
16569
17873
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
16570
17874
  });
16571
17875
  import { Database as Database2 } from "bun:sqlite";
16572
- import { existsSync as existsSync5, mkdirSync as mkdirSync8 } from "fs";
16573
- import { dirname, join as join9, resolve } from "path";
16574
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
16575
- import { homedir as homedir9 } from "os";
17876
+ import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
17877
+ import { dirname, join as join11, resolve } from "path";
17878
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
17879
+ import { homedir as homedir11 } from "os";
16576
17880
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
16577
17881
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
16578
17882
  import { homedir as homedir22 } from "os";
@@ -16586,8 +17890,8 @@ function isInMemoryDb(path) {
16586
17890
  function findNearestMementosDb(startDir) {
16587
17891
  let dir = resolve(startDir);
16588
17892
  while (true) {
16589
- const candidate = join9(dir, ".mementos", "mementos.db");
16590
- if (existsSync5(candidate))
17893
+ const candidate = join11(dir, ".mementos", "mementos.db");
17894
+ if (existsSync6(candidate))
16591
17895
  return candidate;
16592
17896
  const parent = dirname(dir);
16593
17897
  if (parent === dir)
@@ -16599,7 +17903,7 @@ function findNearestMementosDb(startDir) {
16599
17903
  function findGitRoot(startDir) {
16600
17904
  let dir = resolve(startDir);
16601
17905
  while (true) {
16602
- if (existsSync5(join9(dir, ".git")))
17906
+ if (existsSync6(join11(dir, ".git")))
16603
17907
  return dir;
16604
17908
  const parent = dirname(dir);
16605
17909
  if (parent === dir)
@@ -16619,25 +17923,25 @@ function getDbPath() {
16619
17923
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
16620
17924
  const gitRoot = findGitRoot(cwd);
16621
17925
  if (gitRoot) {
16622
- return join9(gitRoot, ".mementos", "mementos.db");
17926
+ return join11(gitRoot, ".mementos", "mementos.db");
16623
17927
  }
16624
17928
  }
16625
17929
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
16626
- return join9(home, ".mementos", "mementos.db");
17930
+ return join11(home, ".mementos", "mementos.db");
16627
17931
  }
16628
- function ensureDir(filePath) {
17932
+ function ensureDir2(filePath) {
16629
17933
  if (isInMemoryDb(filePath))
16630
17934
  return;
16631
17935
  const dir = dirname(resolve(filePath));
16632
- if (!existsSync5(dir)) {
16633
- mkdirSync8(dir, { recursive: true });
17936
+ if (!existsSync6(dir)) {
17937
+ mkdirSync10(dir, { recursive: true });
16634
17938
  }
16635
17939
  }
16636
17940
  function getDatabase2(dbPath) {
16637
17941
  if (_db2)
16638
17942
  return _db2;
16639
17943
  const path = dbPath || getDbPath();
16640
- ensureDir(path);
17944
+ ensureDir2(path);
16641
17945
  _db2 = new Database2(path, { create: true });
16642
17946
  _db2.run("PRAGMA journal_mode = WAL");
16643
17947
  _db2.run("PRAGMA busy_timeout = 5000");
@@ -18480,7 +19784,7 @@ function isValidCategory(value) {
18480
19784
  return VALID_CATEGORIES.includes(value);
18481
19785
  }
18482
19786
  function loadConfig() {
18483
- const configPath = join22(homedir9(), ".mementos", "config.json");
19787
+ const configPath = join22(homedir11(), ".mementos", "config.json");
18484
19788
  let fileConfig = {};
18485
19789
  if (existsSync22(configPath)) {
18486
19790
  try {
@@ -18507,10 +19811,10 @@ function loadConfig() {
18507
19811
  return merged;
18508
19812
  }
18509
19813
  function profilesDir() {
18510
- return join22(homedir9(), ".mementos", "profiles");
19814
+ return join22(homedir11(), ".mementos", "profiles");
18511
19815
  }
18512
19816
  function globalConfigPath() {
18513
- return join22(homedir9(), ".mementos", "config.json");
19817
+ return join22(homedir11(), ".mementos", "config.json");
18514
19818
  }
18515
19819
  function readGlobalConfig() {
18516
19820
  const p = globalConfigPath();
@@ -18524,8 +19828,8 @@ function readGlobalConfig() {
18524
19828
  }
18525
19829
  function writeGlobalConfig(data) {
18526
19830
  const p = globalConfigPath();
18527
- ensureDir2(dirname2(p));
18528
- writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
19831
+ ensureDir22(dirname2(p));
19832
+ writeFileSync4(p, JSON.stringify(data, null, 2), "utf-8");
18529
19833
  }
18530
19834
  function getActiveProfile() {
18531
19835
  const envProfile = process.env["MEMENTOS_PROFILE"];
@@ -18547,18 +19851,18 @@ function listProfiles2() {
18547
19851
  const dir = profilesDir();
18548
19852
  if (!existsSync22(dir))
18549
19853
  return [];
18550
- return readdirSync3(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
19854
+ return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
18551
19855
  }
18552
19856
  function deleteProfile2(name) {
18553
19857
  const dbPath = join22(profilesDir(), `${name}.db`);
18554
19858
  if (!existsSync22(dbPath))
18555
19859
  return false;
18556
- unlinkSync2(dbPath);
19860
+ unlinkSync3(dbPath);
18557
19861
  if (getActiveProfile() === name)
18558
19862
  setActiveProfile(null);
18559
19863
  return true;
18560
19864
  }
18561
- function ensureDir2(dir) {
19865
+ function ensureDir22(dir) {
18562
19866
  if (!existsSync22(dir)) {
18563
19867
  mkdirSync22(dir, { recursive: true });
18564
19868
  }
@@ -19680,7 +20984,7 @@ function writeConfig(config) {
19680
20984
  }
19681
20985
  function getActiveModel() {
19682
20986
  const config = readConfig();
19683
- return config.activeModel ?? DEFAULT_MODEL;
20987
+ return config.activeModel ?? DEFAULT_MODEL2;
19684
20988
  }
19685
20989
  function setActiveModel(modelId) {
19686
20990
  const config = readConfig();
@@ -19754,7 +21058,7 @@ Return JSON with this exact shape:
19754
21058
  examples: finalExamples,
19755
21059
  count: finalExamples.length
19756
21060
  };
19757
- }, DEFAULT_MODEL = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
21061
+ }, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
19758
21062
  var init_dist = __esm(() => {
19759
21063
  __defProp2 = Object.defineProperty;
19760
21064
  exports_database = {};
@@ -21190,10 +22494,10 @@ __export(exports_dist2, {
21190
22494
  acquireLock: () => acquireLock2
21191
22495
  });
21192
22496
  import { Database as Database3 } from "bun:sqlite";
21193
- import { mkdirSync as mkdirSync9 } from "fs";
21194
- import { join as join10, dirname as dirname3 } from "path";
21195
- import { homedir as homedir10 } from "os";
21196
- import { randomUUID as randomUUID10 } from "crypto";
22497
+ import { mkdirSync as mkdirSync11 } from "fs";
22498
+ import { join as join12, dirname as dirname3 } from "path";
22499
+ import { homedir as homedir12 } from "os";
22500
+ import { randomUUID as randomUUID14 } from "crypto";
21197
22501
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
21198
22502
  import { join as join33 } from "path";
21199
22503
  import { homedir as homedir33 } from "os";
@@ -21201,19 +22505,19 @@ import { readFileSync as readFileSync5 } from "fs";
21201
22505
  import { join as join23 } from "path";
21202
22506
  import { homedir as homedir23 } from "os";
21203
22507
  import { randomUUID as randomUUID22 } from "crypto";
21204
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
22508
+ import { readFileSync as readFileSync23, writeFileSync as writeFileSync5, mkdirSync as mkdirSync33 } from "fs";
21205
22509
  import { join as join43, dirname as dirname22 } from "path";
21206
22510
  import { homedir as homedir42 } from "os";
21207
22511
  function getDbPath2() {
21208
22512
  if (process.env.CONVERSATIONS_DB_PATH)
21209
22513
  return process.env.CONVERSATIONS_DB_PATH;
21210
- return join10(homedir10(), ".conversations", "messages.db");
22514
+ return join12(homedir12(), ".conversations", "messages.db");
21211
22515
  }
21212
22516
  function getDb() {
21213
22517
  if (db)
21214
22518
  return db;
21215
22519
  const dbPath = getDbPath2();
21216
- mkdirSync9(dirname3(dbPath), { recursive: true });
22520
+ mkdirSync11(dirname3(dbPath), { recursive: true });
21217
22521
  db = new Database3(dbPath, { create: true });
21218
22522
  db.exec("PRAGMA journal_mode = WAL");
21219
22523
  db.exec("PRAGMA busy_timeout = 5000");
@@ -21573,7 +22877,7 @@ function guessMimeType(name) {
21573
22877
  function sendMessage(opts) {
21574
22878
  const db2 = getDb();
21575
22879
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
21576
- const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID10().slice(0, 8)}`);
22880
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID14().slice(0, 8)}`);
21577
22881
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
21578
22882
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
21579
22883
  const blocking = opts.blocking ? 1 : 0;
@@ -22401,7 +23705,7 @@ function getAutoName() {
22401
23705
  cachedAutoName = name;
22402
23706
  try {
22403
23707
  mkdirSync33(dirname22(AGENT_ID_FILE), { recursive: true });
22404
- writeFileSync4(AGENT_ID_FILE, name + `
23708
+ writeFileSync5(AGENT_ID_FILE, name + `
22405
23709
  `, "utf-8");
22406
23710
  } catch {}
22407
23711
  return name;
@@ -24356,7 +25660,7 @@ Your code should look like:
24356
25660
  }
24357
25661
  }
24358
25662
  }
24359
- function checkPropTypes(typeSpecs, values, location, componentName, element) {
25663
+ function checkPropTypes(typeSpecs, values, location2, componentName, element) {
24360
25664
  {
24361
25665
  var has = Function.call.bind(hasOwnProperty);
24362
25666
  for (var typeSpecName in typeSpecs) {
@@ -24364,23 +25668,23 @@ Your code should look like:
24364
25668
  var error$1 = undefined;
24365
25669
  try {
24366
25670
  if (typeof typeSpecs[typeSpecName] !== "function") {
24367
- 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`.");
25671
+ 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`.");
24368
25672
  err.name = "Invariant Violation";
24369
25673
  throw err;
24370
25674
  }
24371
- error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
25675
+ error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location2, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
24372
25676
  } catch (ex) {
24373
25677
  error$1 = ex;
24374
25678
  }
24375
25679
  if (error$1 && !(error$1 instanceof Error)) {
24376
25680
  setCurrentlyValidatingElement(element);
24377
- 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);
25681
+ 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);
24378
25682
  setCurrentlyValidatingElement(null);
24379
25683
  }
24380
25684
  if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
24381
25685
  loggedTypeFailures[error$1.message] = true;
24382
25686
  setCurrentlyValidatingElement(element);
24383
- error("Failed %s type: %s", location, error$1.message);
25687
+ error("Failed %s type: %s", location2, error$1.message);
24384
25688
  setCurrentlyValidatingElement(null);
24385
25689
  }
24386
25690
  }
@@ -25615,11 +26919,11 @@ __export(exports_dist3, {
25615
26919
  AgentNotFoundError: () => AgentNotFoundError2
25616
26920
  });
25617
26921
  import { Database as Database4 } from "bun:sqlite";
25618
- import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
25619
- import { dirname as dirname5, join as join11, resolve as resolve3 } from "path";
26922
+ import { existsSync as existsSync7, mkdirSync as mkdirSync12 } from "fs";
26923
+ import { dirname as dirname5, join as join13, resolve as resolve3 } from "path";
25620
26924
  import { existsSync as existsSync33 } from "fs";
25621
26925
  import { join as join34 } from "path";
25622
- import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
26926
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
25623
26927
  import { join as join24 } from "path";
25624
26928
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
25625
26929
  import { join as join44 } from "path";
@@ -25849,8 +27153,8 @@ function isInMemoryDb2(path) {
25849
27153
  function findNearestTodosDb(startDir) {
25850
27154
  let dir = resolve3(startDir);
25851
27155
  while (true) {
25852
- const candidate = join11(dir, ".todos", "todos.db");
25853
- if (existsSync6(candidate))
27156
+ const candidate = join13(dir, ".todos", "todos.db");
27157
+ if (existsSync7(candidate))
25854
27158
  return candidate;
25855
27159
  const parent = dirname5(dir);
25856
27160
  if (parent === dir)
@@ -25862,7 +27166,7 @@ function findNearestTodosDb(startDir) {
25862
27166
  function findGitRoot2(startDir) {
25863
27167
  let dir = resolve3(startDir);
25864
27168
  while (true) {
25865
- if (existsSync6(join11(dir, ".git")))
27169
+ if (existsSync7(join13(dir, ".git")))
25866
27170
  return dir;
25867
27171
  const parent = dirname5(dir);
25868
27172
  if (parent === dir)
@@ -25882,18 +27186,18 @@ function getDbPath3() {
25882
27186
  if (process.env["TODOS_DB_SCOPE"] === "project") {
25883
27187
  const gitRoot = findGitRoot2(cwd);
25884
27188
  if (gitRoot) {
25885
- return join11(gitRoot, ".todos", "todos.db");
27189
+ return join13(gitRoot, ".todos", "todos.db");
25886
27190
  }
25887
27191
  }
25888
27192
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
25889
- return join11(home, ".todos", "todos.db");
27193
+ return join13(home, ".todos", "todos.db");
25890
27194
  }
25891
27195
  function ensureDir3(filePath) {
25892
27196
  if (isInMemoryDb2(filePath))
25893
27197
  return;
25894
27198
  const dir = dirname5(resolve3(filePath));
25895
- if (!existsSync6(dir)) {
25896
- mkdirSync10(dir, { recursive: true });
27199
+ if (!existsSync7(dir)) {
27200
+ mkdirSync12(dir, { recursive: true });
25897
27201
  }
25898
27202
  }
25899
27203
  function getDatabase3(dbPath) {
@@ -26352,14 +27656,14 @@ function ensureProject2(name, path, db2) {
26352
27656
  }
26353
27657
  return createProject3({ name, path }, d);
26354
27658
  }
26355
- function ensureDir22(dir) {
27659
+ function ensureDir23(dir) {
26356
27660
  if (!existsSync23(dir))
26357
27661
  mkdirSync24(dir, { recursive: true });
26358
27662
  }
26359
27663
  function listJsonFiles(dir) {
26360
27664
  if (!existsSync23(dir))
26361
27665
  return [];
26362
- return readdirSync4(dir).filter((f) => f.endsWith(".json"));
27666
+ return readdirSync5(dir).filter((f) => f.endsWith(".json"));
26363
27667
  }
26364
27668
  function readJsonFile(path) {
26365
27669
  try {
@@ -26369,7 +27673,7 @@ function readJsonFile(path) {
26369
27673
  }
26370
27674
  }
26371
27675
  function writeJsonFile(path, data) {
26372
- writeFileSync5(path, JSON.stringify(data, null, 2) + `
27676
+ writeFileSync6(path, JSON.stringify(data, null, 2) + `
26373
27677
  `);
26374
27678
  }
26375
27679
  function readHighWaterMark(dir) {
@@ -26380,7 +27684,7 @@ function readHighWaterMark(dir) {
26380
27684
  return isNaN(val) ? 1 : val;
26381
27685
  }
26382
27686
  function writeHighWaterMark(dir, value) {
26383
- writeFileSync5(join24(dir, ".highwatermark"), String(value));
27687
+ writeFileSync6(join24(dir, ".highwatermark"), String(value));
26384
27688
  }
26385
27689
  function getFileMtimeMs(path) {
26386
27690
  try {
@@ -29224,7 +30528,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
29224
30528
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
29225
30529
  const dir = getTaskListDir(taskListId);
29226
30530
  if (!existsSync43(dir))
29227
- ensureDir22(dir);
30531
+ ensureDir23(dir);
29228
30532
  const filter = {};
29229
30533
  if (projectId)
29230
30534
  filter["project_id"] = projectId;
@@ -29442,7 +30746,7 @@ function metadataKey(agent) {
29442
30746
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
29443
30747
  const dir = getTaskListDir2(agent, taskListId);
29444
30748
  if (!existsSync52(dir))
29445
- ensureDir22(dir);
30749
+ ensureDir23(dir);
29446
30750
  const filter = {};
29447
30751
  if (projectId)
29448
30752
  filter["project_id"] = projectId;
@@ -30970,9 +32274,9 @@ __export(exports_dist4, {
30970
32274
  CATEGORIES: () => CATEGORIES,
30971
32275
  AGENT_TARGETS: () => AGENT_TARGETS
30972
32276
  });
30973
- 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";
30974
- import { join as join12, dirname as dirname6 } from "path";
30975
- import { homedir as homedir11 } from "os";
32277
+ 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";
32278
+ import { join as join14, dirname as dirname6 } from "path";
32279
+ import { homedir as homedir13 } from "os";
30976
32280
  import { fileURLToPath } from "url";
30977
32281
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
30978
32282
  import { join as join25 } from "path";
@@ -31083,35 +32387,35 @@ function normalizeSkillName(name) {
31083
32387
  function findSkillsDir() {
31084
32388
  let dir = __dirname2;
31085
32389
  for (let i = 0;i < 5; i++) {
31086
- const candidate = join12(dir, "skills");
31087
- if (existsSync7(candidate)) {
32390
+ const candidate = join14(dir, "skills");
32391
+ if (existsSync8(candidate)) {
31088
32392
  return candidate;
31089
32393
  }
31090
32394
  dir = dirname6(dir);
31091
32395
  }
31092
- return join12(__dirname2, "..", "skills");
32396
+ return join14(__dirname2, "..", "skills");
31093
32397
  }
31094
32398
  function getSkillPath(name) {
31095
32399
  const skillName = normalizeSkillName(name);
31096
- return join12(SKILLS_DIR, skillName);
32400
+ return join14(SKILLS_DIR, skillName);
31097
32401
  }
31098
32402
  function skillExists(name) {
31099
- return existsSync7(getSkillPath(name));
32403
+ return existsSync8(getSkillPath(name));
31100
32404
  }
31101
32405
  function installSkill(name, options = {}) {
31102
32406
  const { targetDir = process.cwd(), overwrite = false } = options;
31103
32407
  const skillName = normalizeSkillName(name);
31104
32408
  const sourcePath = getSkillPath(name);
31105
- const destDir = join12(targetDir, ".skills");
31106
- const destPath = join12(destDir, skillName);
31107
- if (!existsSync7(sourcePath)) {
32409
+ const destDir = join14(targetDir, ".skills");
32410
+ const destPath = join14(destDir, skillName);
32411
+ if (!existsSync8(sourcePath)) {
31108
32412
  return {
31109
32413
  skill: name,
31110
32414
  success: false,
31111
32415
  error: `Skill '${name}' not found`
31112
32416
  };
31113
32417
  }
31114
- if (existsSync7(destPath) && !overwrite) {
32418
+ if (existsSync8(destPath) && !overwrite) {
31115
32419
  return {
31116
32420
  skill: name,
31117
32421
  success: false,
@@ -31120,10 +32424,10 @@ function installSkill(name, options = {}) {
31120
32424
  };
31121
32425
  }
31122
32426
  try {
31123
- if (!existsSync7(destDir)) {
31124
- mkdirSync11(destDir, { recursive: true });
32427
+ if (!existsSync8(destDir)) {
32428
+ mkdirSync13(destDir, { recursive: true });
31125
32429
  }
31126
- if (existsSync7(destPath) && overwrite) {
32430
+ if (existsSync8(destPath) && overwrite) {
31127
32431
  rmSync2(destPath, { recursive: true, force: true });
31128
32432
  }
31129
32433
  cpSync(sourcePath, destPath, {
@@ -31162,10 +32466,10 @@ function installSkills(names, options = {}) {
31162
32466
  return names.map((name) => installSkill(name, options));
31163
32467
  }
31164
32468
  function updateSkillsIndex(skillsDir) {
31165
- const indexPath = join12(skillsDir, "index.ts");
32469
+ const indexPath = join14(skillsDir, "index.ts");
31166
32470
  const meta = loadMeta(skillsDir);
31167
32471
  const disabledSet = new Set(meta.disabled || []);
31168
- const skills = readdirSync5(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
32472
+ const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
31169
32473
  const exports = skills.map((s) => {
31170
32474
  const name = s.replace("skill-", "").replace(/-/g, "_");
31171
32475
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -31178,14 +32482,14 @@ function updateSkillsIndex(skillsDir) {
31178
32482
 
31179
32483
  ${exports}
31180
32484
  `;
31181
- writeFileSync6(indexPath, content);
32485
+ writeFileSync7(indexPath, content);
31182
32486
  }
31183
32487
  function getMetaPath(skillsDir) {
31184
- return join12(skillsDir, ".meta.json");
32488
+ return join14(skillsDir, ".meta.json");
31185
32489
  }
31186
32490
  function loadMeta(skillsDir) {
31187
32491
  const metaPath2 = getMetaPath(skillsDir);
31188
- if (existsSync7(metaPath2)) {
32492
+ if (existsSync8(metaPath2)) {
31189
32493
  try {
31190
32494
  return JSON.parse(readFileSync7(metaPath2, "utf-8"));
31191
32495
  } catch {}
@@ -31193,15 +32497,15 @@ function loadMeta(skillsDir) {
31193
32497
  return { skills: {} };
31194
32498
  }
31195
32499
  function saveMeta(skillsDir, meta) {
31196
- writeFileSync6(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
32500
+ writeFileSync7(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
31197
32501
  }
31198
32502
  function recordInstall(skillsDir, name) {
31199
32503
  const meta = loadMeta(skillsDir);
31200
32504
  const skillName = normalizeSkillName(name);
31201
32505
  let version = "unknown";
31202
32506
  try {
31203
- const pkgPath = join12(skillsDir, skillName, "package.json");
31204
- if (existsSync7(pkgPath)) {
32507
+ const pkgPath = join14(skillsDir, skillName, "package.json");
32508
+ if (existsSync8(pkgPath)) {
31205
32509
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31206
32510
  version = pkg.version || "unknown";
31207
32511
  }
@@ -31215,12 +32519,12 @@ function recordRemove(skillsDir, name) {
31215
32519
  saveMeta(skillsDir, meta);
31216
32520
  }
31217
32521
  function getInstallMeta(targetDir = process.cwd()) {
31218
- return loadMeta(join12(targetDir, ".skills"));
32522
+ return loadMeta(join14(targetDir, ".skills"));
31219
32523
  }
31220
32524
  function disableSkill(name, targetDir = process.cwd()) {
31221
- const skillsDir = join12(targetDir, ".skills");
32525
+ const skillsDir = join14(targetDir, ".skills");
31222
32526
  const skillName = normalizeSkillName(name);
31223
- if (!existsSync7(join12(skillsDir, skillName)))
32527
+ if (!existsSync8(join14(skillsDir, skillName)))
31224
32528
  return false;
31225
32529
  const meta = loadMeta(skillsDir);
31226
32530
  const disabled = new Set(meta.disabled || []);
@@ -31233,7 +32537,7 @@ function disableSkill(name, targetDir = process.cwd()) {
31233
32537
  return true;
31234
32538
  }
31235
32539
  function enableSkill(name, targetDir = process.cwd()) {
31236
- const skillsDir = join12(targetDir, ".skills");
32540
+ const skillsDir = join14(targetDir, ".skills");
31237
32541
  const meta = loadMeta(skillsDir);
31238
32542
  const disabled = new Set(meta.disabled || []);
31239
32543
  if (!disabled.has(name))
@@ -31245,24 +32549,24 @@ function enableSkill(name, targetDir = process.cwd()) {
31245
32549
  return true;
31246
32550
  }
31247
32551
  function getDisabledSkills(targetDir = process.cwd()) {
31248
- const meta = loadMeta(join12(targetDir, ".skills"));
32552
+ const meta = loadMeta(join14(targetDir, ".skills"));
31249
32553
  return meta.disabled || [];
31250
32554
  }
31251
32555
  function getInstalledSkills(targetDir = process.cwd()) {
31252
- const skillsDir = join12(targetDir, ".skills");
31253
- if (!existsSync7(skillsDir)) {
32556
+ const skillsDir = join14(targetDir, ".skills");
32557
+ if (!existsSync8(skillsDir)) {
31254
32558
  return [];
31255
32559
  }
31256
- return readdirSync5(skillsDir).filter((f) => {
31257
- const fullPath = join12(skillsDir, f);
32560
+ return readdirSync6(skillsDir).filter((f) => {
32561
+ const fullPath = join14(skillsDir, f);
31258
32562
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
31259
32563
  }).map((f) => f.replace("skill-", ""));
31260
32564
  }
31261
32565
  function removeSkill(name, targetDir = process.cwd()) {
31262
32566
  const skillName = normalizeSkillName(name);
31263
- const skillsDir = join12(targetDir, ".skills");
31264
- const skillPath = join12(skillsDir, skillName);
31265
- if (!existsSync7(skillPath)) {
32567
+ const skillsDir = join14(targetDir, ".skills");
32568
+ const skillPath = join14(skillsDir, skillName);
32569
+ if (!existsSync8(skillPath)) {
31266
32570
  return false;
31267
32571
  }
31268
32572
  rmSync2(skillPath, { recursive: true, force: true });
@@ -31273,24 +32577,24 @@ function removeSkill(name, targetDir = process.cwd()) {
31273
32577
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
31274
32578
  const agentDir = `.${agent}`;
31275
32579
  if (scope === "project") {
31276
- return join12(projectDir || process.cwd(), agentDir, "skills");
32580
+ return join14(projectDir || process.cwd(), agentDir, "skills");
31277
32581
  }
31278
- return join12(homedir11(), agentDir, "skills");
32582
+ return join14(homedir13(), agentDir, "skills");
31279
32583
  }
31280
32584
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
31281
32585
  const skillName = normalizeSkillName(name);
31282
- return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
32586
+ return join14(getAgentSkillsDir(agent, scope, projectDir), skillName);
31283
32587
  }
31284
32588
  function installSkillForAgent(name, options, generateSkillMd) {
31285
32589
  const { agent, scope = "global", projectDir } = options;
31286
32590
  const skillName = normalizeSkillName(name);
31287
32591
  const sourcePath = getSkillPath(name);
31288
- if (!existsSync7(sourcePath)) {
32592
+ if (!existsSync8(sourcePath)) {
31289
32593
  return { skill: name, success: false, error: `Skill '${name}' not found` };
31290
32594
  }
31291
32595
  let skillMdContent = null;
31292
- const skillMdPath = join12(sourcePath, "SKILL.md");
31293
- if (existsSync7(skillMdPath)) {
32596
+ const skillMdPath = join14(sourcePath, "SKILL.md");
32597
+ if (existsSync8(skillMdPath)) {
31294
32598
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
31295
32599
  } else if (generateSkillMd) {
31296
32600
  skillMdContent = generateSkillMd(name);
@@ -31300,8 +32604,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31300
32604
  }
31301
32605
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31302
32606
  if (scope === "global") {
31303
- const agentBaseDir2 = join12(homedir11(), `.${agent}`);
31304
- if (!existsSync7(agentBaseDir2)) {
32607
+ const agentBaseDir2 = join14(homedir13(), `.${agent}`);
32608
+ if (!existsSync8(agentBaseDir2)) {
31305
32609
  const agentLabels = {
31306
32610
  claude: "Claude Code",
31307
32611
  codex: "Codex CLI",
@@ -31324,8 +32628,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31324
32628
  }
31325
32629
  }
31326
32630
  try {
31327
- mkdirSync11(destDir, { recursive: true });
31328
- writeFileSync6(join12(destDir, "SKILL.md"), skillMdContent);
32631
+ mkdirSync13(destDir, { recursive: true });
32632
+ writeFileSync7(join14(destDir, "SKILL.md"), skillMdContent);
31329
32633
  return { skill: name, success: true, path: destDir };
31330
32634
  } catch (error) {
31331
32635
  return {
@@ -31338,7 +32642,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
31338
32642
  function removeSkillForAgent(name, options) {
31339
32643
  const { agent, scope = "global", projectDir } = options;
31340
32644
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31341
- if (!existsSync7(destDir)) {
32645
+ if (!existsSync8(destDir)) {
31342
32646
  return false;
31343
32647
  }
31344
32648
  rmSync2(destDir, { recursive: true, force: true });
@@ -33262,7 +34566,7 @@ __export(exports_cron_manager, {
33262
34566
  deleteCronJob: () => deleteCronJob,
33263
34567
  createCronJob: () => createCronJob
33264
34568
  });
33265
- import { randomUUID as randomUUID11 } from "crypto";
34569
+ import { randomUUID as randomUUID15 } from "crypto";
33266
34570
  function ensureCronTable() {
33267
34571
  const db2 = getDatabase();
33268
34572
  db2.exec(`
@@ -33291,7 +34595,7 @@ function ensureCronTable() {
33291
34595
  function createCronJob(schedule, task, name) {
33292
34596
  ensureCronTable();
33293
34597
  const db2 = getDatabase();
33294
- const id = randomUUID11();
34598
+ const id = randomUUID15();
33295
34599
  db2.prepare(`
33296
34600
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
33297
34601
  VALUES (?, ?, ?, ?, 1)
@@ -33349,7 +34653,7 @@ function getCronEvents(jobId, limit = 10) {
33349
34653
  async function executeCronJob(job) {
33350
34654
  ensureCronTable();
33351
34655
  const db2 = getDatabase();
33352
- const eventId = randomUUID11();
34656
+ const eventId = randomUUID15();
33353
34657
  const startedAt = new Date().toISOString();
33354
34658
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
33355
34659
  try {
@@ -33440,7 +34744,7 @@ __export(exports_url_watcher, {
33440
34744
  deleteWatchJob: () => deleteWatchJob,
33441
34745
  createWatchJob: () => createWatchJob
33442
34746
  });
33443
- import { randomUUID as randomUUID12 } from "crypto";
34747
+ import { randomUUID as randomUUID16 } from "crypto";
33444
34748
  import { createHash } from "crypto";
33445
34749
  function ensureWatchTables() {
33446
34750
  const db2 = getDatabase();
@@ -33472,7 +34776,7 @@ function ensureWatchTables() {
33472
34776
  function createWatchJob(url, schedule, opts) {
33473
34777
  ensureWatchTables();
33474
34778
  const db2 = getDatabase();
33475
- const id = randomUUID12();
34779
+ const id = randomUUID16();
33476
34780
  db2.prepare(`
33477
34781
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
33478
34782
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -33508,7 +34812,7 @@ function getWatchEvents(watchId, limit = 20) {
33508
34812
  async function checkWatchJob(job) {
33509
34813
  ensureWatchTables();
33510
34814
  const db2 = getDatabase();
33511
- const eventId = randomUUID12();
34815
+ const eventId = randomUUID16();
33512
34816
  const checkedAt = new Date().toISOString();
33513
34817
  let newContent = "";
33514
34818
  try {
@@ -33684,7 +34988,7 @@ var exports_mcp = {};
33684
34988
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33685
34989
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33686
34990
  import { readFileSync as readFileSync8 } from "fs";
33687
- import { join as join13 } from "path";
34991
+ import { join as join15 } from "path";
33688
34992
  function json(data) {
33689
34993
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33690
34994
  }
@@ -33749,7 +35053,7 @@ var init_mcp = __esm(async () => {
33749
35053
  init_dialogs();
33750
35054
  init_profiles();
33751
35055
  init_types();
33752
- _pkg = JSON.parse(readFileSync8(join13(import.meta.dir, "../../package.json"), "utf8"));
35056
+ _pkg = JSON.parse(readFileSync8(join15(import.meta.dir, "../../package.json"), "utf8"));
33753
35057
  networkLogCleanup = new Map;
33754
35058
  consoleCaptureCleanup = new Map;
33755
35059
  harCaptures = new Map;
@@ -33757,7 +35061,7 @@ var init_mcp = __esm(async () => {
33757
35061
  name: "@hasna/browser",
33758
35062
  version: "0.0.1"
33759
35063
  });
33760
- 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.", {
35064
+ 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.", {
33761
35065
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
33762
35066
  use_case: exports_external.string().optional(),
33763
35067
  project_id: exports_external.string().optional(),
@@ -33768,9 +35072,11 @@ var init_mcp = __esm(async () => {
33768
35072
  viewport_height: exports_external.number().optional().default(720),
33769
35073
  stealth: exports_external.boolean().optional().default(false),
33770
35074
  auto_gallery: exports_external.boolean().optional().default(false),
35075
+ storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
33771
35076
  force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
33772
- tags: exports_external.array(exports_external.string()).optional()
33773
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth, auto_gallery, force_new, tags }) => {
35077
+ tags: exports_external.array(exports_external.string()).optional(),
35078
+ 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")
35079
+ }, 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 }) => {
33774
35080
  try {
33775
35081
  if (agent_id && !force_new) {
33776
35082
  const existing = getActiveSessionForAgent2(agent_id);
@@ -33786,7 +35092,9 @@ var init_mcp = __esm(async () => {
33786
35092
  headless,
33787
35093
  viewport: { width: viewport_width, height: viewport_height },
33788
35094
  stealth,
33789
- autoGallery: auto_gallery
35095
+ autoGallery: auto_gallery,
35096
+ storageState: storage_state,
35097
+ cdpUrl: cdp_url
33790
35098
  });
33791
35099
  if (tags?.length) {
33792
35100
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -33946,7 +35254,7 @@ var init_mcp = __esm(async () => {
33946
35254
  return err(e);
33947
35255
  }
33948
35256
  });
33949
- 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 }) => {
35257
+ 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 }) => {
33950
35258
  try {
33951
35259
  const sid = resolveSessionId(session_id);
33952
35260
  const page = getSessionPage(sid);
@@ -33957,14 +35265,17 @@ var init_mcp = __esm(async () => {
33957
35265
  }
33958
35266
  if (!selector)
33959
35267
  return err(new Error("Either ref or selector is required"));
33960
- await click(page, selector, { button, timeout });
33961
- logEvent(sid, "click", { selector, method: "selector" });
35268
+ const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
35269
+ logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
35270
+ if (healInfo.healed) {
35271
+ return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
35272
+ }
33962
35273
  return json({ clicked: selector, method: "selector" });
33963
35274
  } catch (e) {
33964
35275
  return errWithScreenshot(e, session_id);
33965
35276
  }
33966
35277
  });
33967
- 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 }) => {
35278
+ 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 }) => {
33968
35279
  try {
33969
35280
  const sid = resolveSessionId(session_id);
33970
35281
  const page = getSessionPage(sid);
@@ -33975,8 +35286,11 @@ var init_mcp = __esm(async () => {
33975
35286
  }
33976
35287
  if (!selector)
33977
35288
  return err(new Error("Either ref or selector is required"));
33978
- await type(page, selector, text, { clear, delay });
33979
- logEvent(sid, "type", { selector, text: text.slice(0, 100) });
35289
+ const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
35290
+ logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
35291
+ if (healInfo.healed) {
35292
+ return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
35293
+ }
33980
35294
  return json({ typed: text, selector, method: "selector" });
33981
35295
  } catch (e) {
33982
35296
  return errWithScreenshot(e, session_id);
@@ -34070,20 +35384,32 @@ var init_mcp = __esm(async () => {
34070
35384
  return err(e);
34071
35385
  }
34072
35386
  });
34073
- 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 }) => {
35387
+ 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 }) => {
34074
35388
  try {
34075
35389
  const sid = resolveSessionId(session_id);
34076
35390
  const page = getSessionPage(sid);
34077
- return json({ text: await getText(page, selector) });
35391
+ const text = await getText(page, selector);
35392
+ if (sanitize) {
35393
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
35394
+ const sanitized = sanitizeText2(text);
35395
+ return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
35396
+ }
35397
+ return json({ text });
34078
35398
  } catch (e) {
34079
35399
  return err(e);
34080
35400
  }
34081
35401
  });
34082
- 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 }) => {
35402
+ 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 }) => {
34083
35403
  try {
34084
35404
  const sid = resolveSessionId(session_id);
34085
35405
  const page = getSessionPage(sid);
34086
- return json({ html: await getHTML(page, selector) });
35406
+ const html = await getHTML(page, selector);
35407
+ if (sanitize) {
35408
+ const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
35409
+ const sanitized = sanitizeHTML2(html);
35410
+ return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
35411
+ }
35412
+ return json({ html });
34087
35413
  } catch (e) {
34088
35414
  return err(e);
34089
35415
  }
@@ -34098,16 +35424,32 @@ var init_mcp = __esm(async () => {
34098
35424
  return err(e);
34099
35425
  }
34100
35426
  });
34101
- server.tool("browser_extract", "Extract content from the page in a specified format", {
35427
+ server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
34102
35428
  session_id: exports_external.string().optional(),
34103
35429
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
34104
35430
  selector: exports_external.string().optional(),
34105
- schema: exports_external.record(exports_external.string()).optional()
34106
- }, async ({ session_id, format, selector, schema }) => {
35431
+ schema: exports_external.record(exports_external.string()).optional(),
35432
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
35433
+ }, async ({ session_id, format, selector, schema, sanitize }) => {
34107
35434
  try {
34108
35435
  const sid = resolveSessionId(session_id);
34109
35436
  const page = getSessionPage(sid);
34110
35437
  const result = await extract(page, { format, selector, schema });
35438
+ if (sanitize) {
35439
+ const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
35440
+ if (result.text) {
35441
+ const s = sanitizeText2(result.text);
35442
+ result.text = s.text;
35443
+ result.stripped = s.stripped;
35444
+ result.warnings = s.warnings;
35445
+ }
35446
+ if (result.html) {
35447
+ const s = sanitizeHTML2(result.html);
35448
+ result.html = s.text;
35449
+ result.stripped = s.stripped;
35450
+ result.warnings = s.warnings;
35451
+ }
35452
+ }
34111
35453
  return json(result);
34112
35454
  } catch (e) {
34113
35455
  return err(e);
@@ -34124,17 +35466,27 @@ var init_mcp = __esm(async () => {
34124
35466
  return err(e);
34125
35467
  }
34126
35468
  });
34127
- 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.", {
35469
+ 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.", {
34128
35470
  session_id: exports_external.string().optional(),
34129
35471
  compact: exports_external.boolean().optional().default(true),
34130
35472
  max_refs: exports_external.number().optional().default(50),
34131
- full_tree: exports_external.boolean().optional().default(false)
34132
- }, async ({ session_id, compact, max_refs, full_tree }) => {
35473
+ full_tree: exports_external.boolean().optional().default(false),
35474
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
35475
+ }, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
34133
35476
  try {
34134
35477
  const sid = resolveSessionId(session_id);
34135
35478
  const page = getSessionPage(sid);
34136
35479
  const result = await takeSnapshot(page, sid);
34137
35480
  setLastSnapshot(sid, result);
35481
+ let injection_warnings;
35482
+ if (sanitize) {
35483
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
35484
+ const sanitized = sanitizeText2(result.tree);
35485
+ if (sanitized.stripped > 0) {
35486
+ injection_warnings = sanitized.warnings;
35487
+ result.tree = sanitized.text;
35488
+ }
35489
+ }
34138
35490
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
34139
35491
  const limitedRefs = Object.fromEntries(refEntries);
34140
35492
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -34146,27 +35498,29 @@ var init_mcp = __esm(async () => {
34146
35498
  interactive_count: result.interactive_count,
34147
35499
  shown_count: refEntries.length,
34148
35500
  truncated,
34149
- refs: limitedRefs
35501
+ refs: limitedRefs,
35502
+ ...injection_warnings ? { injection_warnings } : {}
34150
35503
  });
34151
35504
  }
34152
35505
  const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
34153
35506
  ... (truncated \u2014 use full_tree=true for complete)` : "");
34154
- return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
35507
+ return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
34155
35508
  } catch (e) {
34156
35509
  return err(e);
34157
35510
  }
34158
35511
  });
34159
- server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
35512
+ 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.", {
34160
35513
  session_id: exports_external.string().optional(),
34161
- selector: exports_external.string().optional(),
35514
+ selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
34162
35515
  full_page: exports_external.boolean().optional().default(false),
34163
35516
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
34164
35517
  quality: exports_external.number().optional().default(60),
34165
35518
  max_width: exports_external.number().optional().default(800),
34166
35519
  compress: exports_external.boolean().optional().default(true),
34167
35520
  thumbnail: exports_external.boolean().optional().default(true),
34168
- annotate: exports_external.boolean().optional().default(false)
34169
- }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
35521
+ annotate: exports_external.boolean().optional().default(false),
35522
+ 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).")
35523
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
34170
35524
  try {
34171
35525
  const sid = resolveSessionId(session_id);
34172
35526
  const page = getSessionPage(sid);
@@ -34183,7 +35537,9 @@ var init_mcp = __esm(async () => {
34183
35537
  annotation_count: annotated.annotations.length
34184
35538
  });
34185
35539
  }
34186
- const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
35540
+ const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
35541
+ const effectiveQuality = detail === "high" ? 75 : quality;
35542
+ const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
34187
35543
  result.url = page.url();
34188
35544
  try {
34189
35545
  const buf = Buffer.from(result.base64, "base64");
@@ -34192,12 +35548,12 @@ var init_mcp = __esm(async () => {
34192
35548
  result.download_id = dl.id;
34193
35549
  } catch {}
34194
35550
  result.estimated_tokens = Math.ceil(result.base64.length / 4);
34195
- if (result.base64.length > 20000) {
35551
+ if (detail !== "high" && result.base64.length > 40000) {
34196
35552
  result.base64_truncated = true;
34197
35553
  result.full_image_path = result.path;
34198
35554
  result.base64 = result.thumbnail_base64 ?? "";
34199
35555
  }
34200
- logEvent(sid, "screenshot", { path: result.path });
35556
+ logEvent(sid, "screenshot", { path: result.path, detail, selector });
34201
35557
  return json(result);
34202
35558
  } catch (e) {
34203
35559
  return err(e);
@@ -34379,6 +35735,28 @@ var init_mcp = __esm(async () => {
34379
35735
  return err(e);
34380
35736
  }
34381
35737
  });
35738
+ 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 }) => {
35739
+ try {
35740
+ const sid = resolveSessionId(session_id);
35741
+ const page = getSessionPage(sid);
35742
+ const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
35743
+ const result = await detectEnvironment2(page);
35744
+ return json(result);
35745
+ } catch (e) {
35746
+ return err(e);
35747
+ }
35748
+ });
35749
+ 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 }) => {
35750
+ try {
35751
+ const sid = resolveSessionId(session_id);
35752
+ const page = getSessionPage(sid);
35753
+ const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
35754
+ const result = await getDeepPerformance2(page);
35755
+ return json(result);
35756
+ } catch (e) {
35757
+ return err(e);
35758
+ }
35759
+ });
34382
35760
  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 }) => {
34383
35761
  try {
34384
35762
  const sid = resolveSessionId(session_id);
@@ -34442,6 +35820,46 @@ var init_mcp = __esm(async () => {
34442
35820
  return err(e);
34443
35821
  }
34444
35822
  });
35823
+ 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 }) => {
35824
+ try {
35825
+ const { saveWorkflowFromRecording: saveWorkflowFromRecording2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35826
+ return json(saveWorkflowFromRecording2(recording_id, name, description));
35827
+ } catch (e) {
35828
+ return err(e);
35829
+ }
35830
+ });
35831
+ server.tool("browser_workflow_list", "List all saved workflows", {}, async () => {
35832
+ try {
35833
+ const { listWorkflows: listWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35834
+ const workflows = listWorkflows2();
35835
+ return json({ workflows: workflows.map((w) => ({ ...w, steps: `${w.steps.length} steps` })), count: workflows.length });
35836
+ } catch (e) {
35837
+ return err(e);
35838
+ }
35839
+ });
35840
+ 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 }) => {
35841
+ try {
35842
+ const sid = resolveSessionId(session_id);
35843
+ const page = getSessionPage(sid);
35844
+ const { getWorkflowByName: getWorkflowByName2, runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35845
+ const workflow = getWorkflowByName2(name);
35846
+ if (!workflow)
35847
+ return err(new Error(`Workflow '${name}' not found`));
35848
+ const result = await runWorkflow2(workflow, page);
35849
+ logEvent(sid, "workflow_run", { name, ...result });
35850
+ return json(result);
35851
+ } catch (e) {
35852
+ return err(e);
35853
+ }
35854
+ });
35855
+ server.tool("browser_workflow_delete", "Delete a saved workflow", { name: exports_external.string() }, async ({ name }) => {
35856
+ try {
35857
+ const { deleteWorkflow: deleteWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
35858
+ return json({ deleted: deleteWorkflow2(name) });
35859
+ } catch (e) {
35860
+ return err(e);
35861
+ }
35862
+ });
34445
35863
  server.tool("browser_crawl", "Crawl a URL recursively and return discovered pages", {
34446
35864
  url: exports_external.string(),
34447
35865
  max_depth: exports_external.number().optional().default(2),
@@ -34603,6 +36021,94 @@ var init_mcp = __esm(async () => {
34603
36021
  return err(e);
34604
36022
  }
34605
36023
  });
36024
+ 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 }) => {
36025
+ try {
36026
+ const sid = resolveSessionId(session_id);
36027
+ const page = getSessionPage(sid);
36028
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36029
+ const path = await saveStateFromPage2(page, name);
36030
+ return json({ saved: true, name, path });
36031
+ } catch (e) {
36032
+ return err(e);
36033
+ }
36034
+ });
36035
+ server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
36036
+ try {
36037
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36038
+ const states = listStates2();
36039
+ return json({ states, count: states.length });
36040
+ } catch (e) {
36041
+ return err(e);
36042
+ }
36043
+ });
36044
+ server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
36045
+ try {
36046
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36047
+ return json({ deleted: deleteState2(name), name });
36048
+ } catch (e) {
36049
+ return err(e);
36050
+ }
36051
+ });
36052
+ 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 }) => {
36053
+ try {
36054
+ const sid = resolveSessionId(session_id);
36055
+ const page = getSessionPage(sid);
36056
+ if (start_url)
36057
+ await navigate(page, start_url);
36058
+ const recording = startRecording(sid, `auth-${name}`, page.url());
36059
+ return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
36060
+ } catch (e) {
36061
+ return err(e);
36062
+ }
36063
+ });
36064
+ 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 }) => {
36065
+ try {
36066
+ const sid = resolveSessionId(session_id);
36067
+ const page = getSessionPage(sid);
36068
+ const recording = stopRecording(recording_id);
36069
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36070
+ const statePath2 = await saveStateFromPage2(page, name);
36071
+ let domain = "";
36072
+ try {
36073
+ domain = new URL(page.url()).hostname;
36074
+ } catch {}
36075
+ const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
36076
+ const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
36077
+ return json({ flow, recording_steps: recording.steps.length });
36078
+ } catch (e) {
36079
+ return err(e);
36080
+ }
36081
+ });
36082
+ 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 }) => {
36083
+ try {
36084
+ const sid = resolveSessionId(session_id);
36085
+ const page = getSessionPage(sid);
36086
+ const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
36087
+ const flow = getAuthFlowByName2(name);
36088
+ if (!flow)
36089
+ return err(new Error(`Auth flow '${name}' not found`));
36090
+ const result = await tryReplayAuth2(page, flow.domain);
36091
+ return json(result);
36092
+ } catch (e) {
36093
+ return err(e);
36094
+ }
36095
+ });
36096
+ server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
36097
+ try {
36098
+ const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
36099
+ return json({ flows: listAuthFlows2() });
36100
+ } catch (e) {
36101
+ return err(e);
36102
+ }
36103
+ });
36104
+ server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
36105
+ try {
36106
+ const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
36107
+ return json({ deleted: deleteAuthFlow2(name) });
36108
+ } catch (e) {
36109
+ return err(e);
36110
+ }
36111
+ });
34606
36112
  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 }) => {
34607
36113
  try {
34608
36114
  const sid = resolveSessionId(session_id);
@@ -34613,20 +36119,45 @@ var init_mcp = __esm(async () => {
34613
36119
  return err(e);
34614
36120
  }
34615
36121
  });
34616
- server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
36122
+ 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.", {
34617
36123
  session_id: exports_external.string().optional(),
34618
36124
  fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
34619
- submit_selector: exports_external.string().optional()
34620
- }, async ({ session_id, fields, submit_selector }) => {
36125
+ submit_selector: exports_external.string().optional(),
36126
+ self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
36127
+ }, async ({ session_id, fields, submit_selector, self_heal }) => {
34621
36128
  try {
34622
36129
  const sid = resolveSessionId(session_id);
34623
36130
  const page = getSessionPage(sid);
34624
- const result = await fillForm(page, fields, submit_selector);
36131
+ const result = await fillForm(page, fields, submit_selector, self_heal);
34625
36132
  return json(result);
34626
36133
  } catch (e) {
34627
36134
  return errWithScreenshot(e, session_id);
34628
36135
  }
34629
36136
  });
36137
+ 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.", {
36138
+ session_id: exports_external.string().optional(),
36139
+ 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')"),
36140
+ click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
36141
+ model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
36142
+ }, async ({ session_id, description, click: doClick, model }) => {
36143
+ try {
36144
+ const sid = resolveSessionId(session_id);
36145
+ const page = getSessionPage(sid);
36146
+ if (doClick) {
36147
+ const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
36148
+ const result = await clickByVision2(page, description, { model });
36149
+ logEvent(sid, "vision_click", { query: description, ...result });
36150
+ return json(result);
36151
+ } else {
36152
+ const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
36153
+ const result = await findElementByVision2(page, description, { model });
36154
+ logEvent(sid, "vision_find", { query: description, ...result });
36155
+ return json(result);
36156
+ }
36157
+ } catch (e) {
36158
+ return err(e);
36159
+ }
36160
+ });
34630
36161
  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 }) => {
34631
36162
  try {
34632
36163
  const sid = resolveSessionId(session_id);
@@ -35024,6 +36555,68 @@ var init_mcp = __esm(async () => {
35024
36555
  return err(e);
35025
36556
  }
35026
36557
  });
36558
+ 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 }) => {
36559
+ try {
36560
+ const sid = resolveSessionId(session_id);
36561
+ const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
36562
+ const apis = detectAPIs2(sid);
36563
+ return json({ apis, count: apis.length });
36564
+ } catch (e) {
36565
+ return err(e);
36566
+ }
36567
+ });
36568
+ 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 }) => {
36569
+ try {
36570
+ const sid = resolveSessionId(session_id);
36571
+ const page = getSessionPage(sid);
36572
+ const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
36573
+ const data = await extractStructuredData2(page);
36574
+ return json({
36575
+ tables: data.tables.length,
36576
+ lists: data.lists.length,
36577
+ json_ld: data.jsonLd.length,
36578
+ open_graph: Object.keys(data.openGraph).length,
36579
+ meta_tags: Object.keys(data.metaTags).length,
36580
+ repeated_elements: data.repeatedElements.length,
36581
+ data
36582
+ });
36583
+ } catch (e) {
36584
+ return err(e);
36585
+ }
36586
+ });
36587
+ 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 }) => {
36588
+ try {
36589
+ const { saveDataset: saveDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36590
+ const dataset = saveDataset2({ name, rows: data, sourceUrl: source_url });
36591
+ return json({ id: dataset.id, name: dataset.name, row_count: dataset.row_count });
36592
+ } catch (e) {
36593
+ return err(e);
36594
+ }
36595
+ });
36596
+ server.tool("browser_dataset_list", "List all saved datasets", {}, async () => {
36597
+ try {
36598
+ const { listDatasets: listDatasets2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36599
+ return json({ datasets: listDatasets2() });
36600
+ } catch (e) {
36601
+ return err(e);
36602
+ }
36603
+ });
36604
+ 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 }) => {
36605
+ try {
36606
+ const { exportDataset: exportDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36607
+ return json(exportDataset2(name, format));
36608
+ } catch (e) {
36609
+ return err(e);
36610
+ }
36611
+ });
36612
+ server.tool("browser_dataset_delete", "Delete a saved dataset", { name: exports_external.string() }, async ({ name }) => {
36613
+ try {
36614
+ const { deleteDataset: deleteDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
36615
+ return json({ deleted: deleteDataset2(name) });
36616
+ } catch (e) {
36617
+ return err(e);
36618
+ }
36619
+ });
35027
36620
  server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
35028
36621
  try {
35029
36622
  const groups = {
@@ -35047,6 +36640,7 @@ var init_mcp = __esm(async () => {
35047
36640
  { tool: "browser_wait", description: "Wait for a selector to appear" },
35048
36641
  { tool: "browser_wait_for_text", description: "Wait for text to appear" },
35049
36642
  { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
36643
+ { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
35050
36644
  { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
35051
36645
  ],
35052
36646
  Extraction: [
@@ -35075,7 +36669,10 @@ var init_mcp = __esm(async () => {
35075
36669
  { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
35076
36670
  { tool: "browser_profile_load", description: "Load and apply a saved profile" },
35077
36671
  { tool: "browser_profile_list", description: "List saved profiles" },
35078
- { tool: "browser_profile_delete", description: "Delete a saved profile" }
36672
+ { tool: "browser_profile_delete", description: "Delete a saved profile" },
36673
+ { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
36674
+ { tool: "browser_session_list_states", description: "List saved storage states" },
36675
+ { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
35079
36676
  ],
35080
36677
  Network: [
35081
36678
  { tool: "browser_network_log", description: "Get captured network requests" },
@@ -35099,6 +36696,27 @@ var init_mcp = __esm(async () => {
35099
36696
  { tool: "browser_record_replay", description: "Replay a recorded sequence" },
35100
36697
  { tool: "browser_recordings_list", description: "List all recordings" }
35101
36698
  ],
36699
+ Auth: [
36700
+ { tool: "browser_auth_record", description: "Start recording a login flow" },
36701
+ { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
36702
+ { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
36703
+ { tool: "browser_auth_list", description: "List all saved auth flows" },
36704
+ { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
36705
+ ],
36706
+ Workflows: [
36707
+ { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
36708
+ { tool: "browser_workflow_list", description: "List all saved workflows" },
36709
+ { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
36710
+ { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
36711
+ ],
36712
+ Data: [
36713
+ { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
36714
+ { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
36715
+ { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
36716
+ { tool: "browser_dataset_list", description: "List all saved datasets" },
36717
+ { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
36718
+ { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
36719
+ ],
35102
36720
  Crawl: [
35103
36721
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
35104
36722
  ],
@@ -35152,10 +36770,13 @@ var init_mcp = __esm(async () => {
35152
36770
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
35153
36771
  { tool: "browser_version", description: "Show running binary version and tool count" },
35154
36772
  { tool: "browser_help", description: "Show this help (all tools)" },
36773
+ { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
36774
+ { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
35155
36775
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
35156
36776
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
35157
36777
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
35158
- { tool: "browser_watch_stop", description: "Stop DOM watcher" }
36778
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" },
36779
+ { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
35159
36780
  ]
35160
36781
  };
35161
36782
  const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
@@ -35438,6 +37059,82 @@ var init_mcp = __esm(async () => {
35438
37059
  return err(e);
35439
37060
  }
35440
37061
  });
37062
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
37063
+ actions: exports_external.array(exports_external.object({
37064
+ session_id: exports_external.string().describe("Target session ID"),
37065
+ tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
37066
+ args: exports_external.record(exports_external.unknown()).optional().default({})
37067
+ })),
37068
+ timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
37069
+ }, async ({ actions, timeout }) => {
37070
+ try {
37071
+ const t0 = Date.now();
37072
+ const promises = actions.map(async (action, index) => {
37073
+ try {
37074
+ const sid = action.session_id;
37075
+ const page = getSessionPage(sid);
37076
+ const args = action.args;
37077
+ const toolName = action.tool.replace(/^browser_/, "");
37078
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
37079
+ const actionPromise = (async () => {
37080
+ switch (toolName) {
37081
+ case "navigate": {
37082
+ await navigate(page, args.url);
37083
+ const title = await page.title();
37084
+ return { url: page.url(), title };
37085
+ }
37086
+ case "screenshot": {
37087
+ const result2 = await takeScreenshot(page, {
37088
+ maxWidth: args.max_width ?? 800,
37089
+ quality: args.quality ?? 60
37090
+ });
37091
+ return { path: result2.path, size_bytes: result2.size_bytes };
37092
+ }
37093
+ case "click": {
37094
+ if (args.selector)
37095
+ await click(page, args.selector);
37096
+ return { clicked: args.selector };
37097
+ }
37098
+ case "type": {
37099
+ if (args.selector && args.text)
37100
+ await type(page, args.selector, args.text);
37101
+ return { typed: args.text };
37102
+ }
37103
+ case "get_text": {
37104
+ const text = await getText(page);
37105
+ return { text: text.slice(0, 1000), length: text.length };
37106
+ }
37107
+ case "get_links": {
37108
+ const links = await getLinks(page);
37109
+ return { links, count: links.length };
37110
+ }
37111
+ case "snapshot": {
37112
+ const snap = await takeSnapshot(page, sid);
37113
+ return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
37114
+ }
37115
+ case "evaluate": {
37116
+ const result2 = await page.evaluate(args.expression);
37117
+ return { result: result2 };
37118
+ }
37119
+ default:
37120
+ return { error: `Unknown tool: ${action.tool}` };
37121
+ }
37122
+ })();
37123
+ const result = await Promise.race([actionPromise, timeoutPromise]);
37124
+ return { index, session_id: sid, tool: action.tool, success: true, result };
37125
+ } catch (e) {
37126
+ return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
37127
+ }
37128
+ });
37129
+ const results = await Promise.all(promises);
37130
+ const duration_ms = Date.now() - t0;
37131
+ const succeeded = results.filter((r) => r.success).length;
37132
+ const failed = results.filter((r) => !r.success).length;
37133
+ return json({ results, duration_ms, succeeded, failed, total: actions.length });
37134
+ } catch (e) {
37135
+ return err(e);
37136
+ }
37137
+ });
35441
37138
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
35442
37139
  try {
35443
37140
  return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
@@ -35592,10 +37289,10 @@ __export(exports_snapshots, {
35592
37289
  deleteSnapshot: () => deleteSnapshot,
35593
37290
  createSnapshot: () => createSnapshot
35594
37291
  });
35595
- import { randomUUID as randomUUID13 } from "crypto";
37292
+ import { randomUUID as randomUUID17 } from "crypto";
35596
37293
  function createSnapshot(data) {
35597
37294
  const db2 = getDatabase();
35598
- const id = randomUUID13();
37295
+ const id = randomUUID17();
35599
37296
  db2.prepare("INSERT INTO snapshots (id, session_id, url, title, html, screenshot_path) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.url, data.title ?? null, data.html ?? null, data.screenshot_path ?? null);
35600
37297
  return getSnapshot(id);
35601
37298
  }
@@ -35621,8 +37318,8 @@ var init_snapshots = __esm(() => {
35621
37318
 
35622
37319
  // src/server/index.ts
35623
37320
  var exports_server = {};
35624
- import { join as join14 } from "path";
35625
- import { existsSync as existsSync8 } from "fs";
37321
+ import { join as join16 } from "path";
37322
+ import { existsSync as existsSync9 } from "fs";
35626
37323
  function ok(data, status = 200) {
35627
37324
  return new Response(JSON.stringify(data), {
35628
37325
  status,
@@ -35872,14 +37569,14 @@ var init_server = __esm(() => {
35872
37569
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
35873
37570
  const id = path.split("/")[3];
35874
37571
  const entry = getEntry(id);
35875
- if (!entry?.thumbnail_path || !existsSync8(entry.thumbnail_path))
37572
+ if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
35876
37573
  return notFound("Thumbnail not found");
35877
37574
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
35878
37575
  }
35879
37576
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
35880
37577
  const id = path.split("/")[3];
35881
37578
  const entry = getEntry(id);
35882
- if (!entry?.path || !existsSync8(entry.path))
37579
+ if (!entry?.path || !existsSync9(entry.path))
35883
37580
  return notFound("Image not found");
35884
37581
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
35885
37582
  }
@@ -35907,7 +37604,7 @@ var init_server = __esm(() => {
35907
37604
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
35908
37605
  const id = path.split("/")[3];
35909
37606
  const file = getDownload(id);
35910
- if (!file || !existsSync8(file.path))
37607
+ if (!file || !existsSync9(file.path))
35911
37608
  return notFound("Download not found");
35912
37609
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
35913
37610
  }
@@ -35915,13 +37612,13 @@ var init_server = __esm(() => {
35915
37612
  const id = path.split("/")[3];
35916
37613
  return ok({ deleted: deleteDownload(id) });
35917
37614
  }
35918
- const dashboardDist = join14(import.meta.dir, "../../dashboard/dist");
35919
- if (existsSync8(dashboardDist)) {
35920
- const filePath = path === "/" ? join14(dashboardDist, "index.html") : join14(dashboardDist, path);
35921
- if (existsSync8(filePath)) {
37615
+ const dashboardDist = join16(import.meta.dir, "../../dashboard/dist");
37616
+ if (existsSync9(dashboardDist)) {
37617
+ const filePath = path === "/" ? join16(dashboardDist, "index.html") : join16(dashboardDist, path);
37618
+ if (existsSync9(filePath)) {
35922
37619
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
35923
37620
  }
35924
- return new Response(Bun.file(join14(dashboardDist, "index.html")), { headers: CORS_HEADERS });
37621
+ return new Response(Bun.file(join16(dashboardDist, "index.html")), { headers: CORS_HEADERS });
35925
37622
  }
35926
37623
  if (path === "/" || path === "") {
35927
37624
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -35964,9 +37661,9 @@ init_recorder();
35964
37661
  init_recordings();
35965
37662
  init_lightpanda();
35966
37663
  import { readFileSync as readFileSync9 } from "fs";
35967
- import { join as join15 } from "path";
37664
+ import { join as join17 } from "path";
35968
37665
  import chalk from "chalk";
35969
- var pkg = JSON.parse(readFileSync9(join15(import.meta.dir, "../../package.json"), "utf8"));
37666
+ var pkg = JSON.parse(readFileSync9(join17(import.meta.dir, "../../package.json"), "utf8"));
35970
37667
  var program2 = new Command;
35971
37668
  program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
35972
37669
  program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
@@ -36100,6 +37797,22 @@ sessionCmd.command("close <id>").description("Close a session").action(async (id
36100
37797
  await closeSession2(id);
36101
37798
  console.log(chalk.green(`\u2713 Session closed: ${id}`));
36102
37799
  });
37800
+ sessionCmd.command("save-state <name>").description("Save current session auth state for reuse").requiredOption("--session <id>", "Session ID").action(async (name, opts) => {
37801
+ const page = getSessionPage(opts.session);
37802
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
37803
+ const path = await saveStateFromPage2(page, name);
37804
+ console.log(chalk.green(`\u2713 State saved: ${name}`));
37805
+ console.log(chalk.gray(` Path: ${path}`));
37806
+ });
37807
+ sessionCmd.command("list-states").description("List saved auth states").action(async () => {
37808
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
37809
+ const states = listStates2();
37810
+ if (states.length === 0) {
37811
+ console.log(chalk.gray("No saved states"));
37812
+ return;
37813
+ }
37814
+ states.forEach((s) => console.log(`${s.name} ${chalk.gray(s.modified)}`));
37815
+ });
36103
37816
  var recordCmd = program2.command("record").description("Manage action recordings");
36104
37817
  recordCmd.command("start <name>").description("Start recording actions in a new session").option("--url <url>", "Start URL").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").action(async (name, opts) => {
36105
37818
  const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
@@ -36162,6 +37875,19 @@ projectCmd.command("list").description("List all projects").action(() => {
36162
37875
  projects.forEach((p) => console.log(`${p.id} "${p.name}" ${p.path}`));
36163
37876
  }
36164
37877
  });
37878
+ program2.command("attach").description("Attach to a running Chrome browser via CDP").option("--port <port>", "Chrome debugging port", "9222").option("--host <host>", "Chrome debugging host", "localhost").option("--json", "Output as JSON").action(async (opts) => {
37879
+ const cdpUrl = `http://${opts.host}:${opts.port}`;
37880
+ const { session, page } = await createSession2({ cdpUrl });
37881
+ const title = await page.title();
37882
+ const url = page.url();
37883
+ if (opts.json) {
37884
+ console.log(JSON.stringify({ session_id: session.id, url, title, cdp_url: cdpUrl }));
37885
+ } else {
37886
+ console.log(chalk.green(`\u2713 Attached to Chrome at ${cdpUrl}`));
37887
+ console.log(chalk.blue(` Session: ${session.id}`));
37888
+ console.log(chalk.blue(` Page: ${title} (${url})`));
37889
+ }
37890
+ });
36165
37891
  program2.command("install-browser").description("Install a browser engine").option("--engine <engine>", "Engine to install: lightpanda|chromium", "chromium").action(async (opts) => {
36166
37892
  if (opts.engine === "chromium") {
36167
37893
  const { execSync: execSync3 } = await import("child_process");
@@ -36253,11 +37979,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
36253
37979
  });
36254
37980
  galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
36255
37981
  const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
36256
- const { existsSync: existsSync9 } = await import("fs");
37982
+ const { existsSync: existsSync10 } = await import("fs");
36257
37983
  const entries = listEntries2({ limit: 9999 });
36258
37984
  let removed = 0;
36259
37985
  for (const e of entries) {
36260
- if (!existsSync9(e.path)) {
37986
+ if (!existsSync10(e.path)) {
36261
37987
  deleteEntry2(e.id);
36262
37988
  removed++;
36263
37989
  }