@hasna/browser 0.0.9 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2341,6 +2341,23 @@ 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
+ `
2344
2361
  }
2345
2362
  ];
2346
2363
  for (const m of migrations) {
@@ -3456,6 +3473,188 @@ var init_dialogs = __esm(() => {
3456
3473
  pendingDialogs = new Map;
3457
3474
  });
3458
3475
 
3476
+ // src/engines/cdp.ts
3477
+ var exports_cdp = {};
3478
+ __export(exports_cdp, {
3479
+ connectToExistingBrowser: () => connectToExistingBrowser,
3480
+ CDPClient: () => CDPClient
3481
+ });
3482
+ async function connectToExistingBrowser(cdpUrl) {
3483
+ const { chromium: chromium3 } = await import("playwright");
3484
+ try {
3485
+ return await chromium3.connectOverCDP(cdpUrl);
3486
+ } catch (err) {
3487
+ 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);
3488
+ }
3489
+ }
3490
+
3491
+ class CDPClient {
3492
+ session;
3493
+ networkEnabled = false;
3494
+ performanceEnabled = false;
3495
+ constructor(session) {
3496
+ this.session = session;
3497
+ }
3498
+ static async fromPage(page) {
3499
+ try {
3500
+ const session = await page.context().newCDPSession(page);
3501
+ return new CDPClient(session);
3502
+ } catch (err) {
3503
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
3504
+ }
3505
+ }
3506
+ async send(method, params) {
3507
+ try {
3508
+ return await this.session.send(method, params);
3509
+ } catch (err) {
3510
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
3511
+ }
3512
+ }
3513
+ on(event, handler) {
3514
+ this.session.on(event, handler);
3515
+ }
3516
+ off(event, handler) {
3517
+ this.session.off(event, handler);
3518
+ }
3519
+ async enableNetwork() {
3520
+ if (!this.networkEnabled) {
3521
+ await this.send("Network.enable");
3522
+ this.networkEnabled = true;
3523
+ }
3524
+ }
3525
+ async enablePerformance() {
3526
+ if (!this.performanceEnabled) {
3527
+ await this.send("Performance.enable");
3528
+ this.performanceEnabled = true;
3529
+ }
3530
+ }
3531
+ async getPerformanceMetrics() {
3532
+ await this.enablePerformance();
3533
+ const result = await this.send("Performance.getMetrics");
3534
+ const m = {};
3535
+ for (const metric of result.metrics) {
3536
+ m[metric.name] = metric.value;
3537
+ }
3538
+ return {
3539
+ js_heap_size_used: m["JSHeapUsedSize"],
3540
+ js_heap_size_total: m["JSHeapTotalSize"],
3541
+ dom_interactive: m["DOMInteractive"],
3542
+ dom_complete: m["DOMComplete"],
3543
+ load_event: m["LoadEventEnd"]
3544
+ };
3545
+ }
3546
+ async startJSCoverage() {
3547
+ await this.send("Profiler.enable");
3548
+ await this.send("Debugger.enable");
3549
+ await this.send("Profiler.startPreciseCoverage", {
3550
+ callCount: false,
3551
+ detailed: true
3552
+ });
3553
+ }
3554
+ async stopJSCoverage() {
3555
+ const result = await this.send("Profiler.takePreciseCoverage");
3556
+ await this.send("Profiler.stopPreciseCoverage");
3557
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
3558
+ url: r.url,
3559
+ text: "",
3560
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
3561
+ }));
3562
+ }
3563
+ async getCoverage() {
3564
+ await this.startJSCoverage();
3565
+ const js = await this.stopJSCoverage();
3566
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
3567
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
3568
+ }
3569
+ async captureHAREntries(page, handler) {
3570
+ await this.enableNetwork();
3571
+ const requestTimings = new Map;
3572
+ const onRequest = (params) => {
3573
+ requestTimings.set(params.requestId, params.timestamp);
3574
+ };
3575
+ const onResponse = (params) => {
3576
+ const start = requestTimings.get(params.requestId);
3577
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
3578
+ handler({
3579
+ method: "GET",
3580
+ url: params.response.url,
3581
+ status: params.response.status,
3582
+ duration
3583
+ });
3584
+ };
3585
+ this.on("Network.requestWillBeSent", onRequest);
3586
+ this.on("Network.responseReceived", onResponse);
3587
+ return () => {
3588
+ this.off("Network.requestWillBeSent", onRequest);
3589
+ this.off("Network.responseReceived", onResponse);
3590
+ };
3591
+ }
3592
+ async detach() {
3593
+ try {
3594
+ await this.session.detach();
3595
+ } catch {}
3596
+ }
3597
+ }
3598
+ var init_cdp = __esm(() => {
3599
+ init_types();
3600
+ });
3601
+
3602
+ // src/lib/storage-state.ts
3603
+ var exports_storage_state = {};
3604
+ __export(exports_storage_state, {
3605
+ saveStateFromPage: () => saveStateFromPage,
3606
+ saveState: () => saveState,
3607
+ loadStatePath: () => loadStatePath,
3608
+ listStates: () => listStates,
3609
+ deleteState: () => deleteState
3610
+ });
3611
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
3612
+ import { join as join3 } from "path";
3613
+ import { homedir as homedir3 } from "os";
3614
+ function ensureDir() {
3615
+ mkdirSync3(STATES_DIR, { recursive: true });
3616
+ }
3617
+ function statePath(name) {
3618
+ return join3(STATES_DIR, `${name}.json`);
3619
+ }
3620
+ async function saveState(context, name) {
3621
+ ensureDir();
3622
+ const path = statePath(name);
3623
+ const state = await context.storageState({ path });
3624
+ return path;
3625
+ }
3626
+ async function saveStateFromPage(page, name) {
3627
+ return saveState(page.context(), name);
3628
+ }
3629
+ function loadStatePath(name) {
3630
+ const path = statePath(name);
3631
+ return existsSync(path) ? path : null;
3632
+ }
3633
+ function listStates() {
3634
+ ensureDir();
3635
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
3636
+ const path = join3(STATES_DIR, f);
3637
+ const stat = Bun.file(path);
3638
+ return {
3639
+ name: f.replace(".json", ""),
3640
+ path,
3641
+ modified: new Date(stat.lastModified).toISOString()
3642
+ };
3643
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
3644
+ }
3645
+ function deleteState(name) {
3646
+ const path = statePath(name);
3647
+ if (existsSync(path)) {
3648
+ unlinkSync(path);
3649
+ return true;
3650
+ }
3651
+ return false;
3652
+ }
3653
+ var STATES_DIR;
3654
+ var init_storage_state = __esm(() => {
3655
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
3656
+ });
3657
+
3459
3658
  // src/lib/session.ts
3460
3659
  var exports_session = {};
3461
3660
  __export(exports_session, {
@@ -3485,6 +3684,37 @@ function createBunProxy(view) {
3485
3684
  return view;
3486
3685
  }
3487
3686
  async function createSession2(opts = {}) {
3687
+ if (opts.cdpUrl) {
3688
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
3689
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
3690
+ const contexts = cdpBrowser.contexts();
3691
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
3692
+ const pages = context.pages();
3693
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
3694
+ const session2 = createSession({
3695
+ engine: "cdp",
3696
+ projectId: opts.projectId,
3697
+ agentId: opts.agentId,
3698
+ startUrl: page2.url(),
3699
+ name: opts.name ?? "attached"
3700
+ });
3701
+ const cleanups2 = [];
3702
+ if (opts.captureNetwork !== false) {
3703
+ try {
3704
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
3705
+ } catch {}
3706
+ }
3707
+ if (opts.captureConsole !== false) {
3708
+ try {
3709
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
3710
+ } catch {}
3711
+ }
3712
+ try {
3713
+ cleanups2.push(setupDialogHandler(page2, session2.id));
3714
+ } catch {}
3715
+ 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 });
3716
+ return { session: session2, page: page2 };
3717
+ }
3488
3718
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
3489
3719
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
3490
3720
  let browser = null;
@@ -3510,7 +3740,22 @@ async function createSession2(opts = {}) {
3510
3740
  page = await context.newPage();
3511
3741
  } else {
3512
3742
  browser = await pool.acquire(opts.headless ?? true);
3513
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3743
+ if (opts.storageState) {
3744
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
3745
+ const statePath2 = loadStatePath2(opts.storageState);
3746
+ if (statePath2) {
3747
+ const context = await browser.newContext({
3748
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
3749
+ userAgent: opts.userAgent,
3750
+ storageState: statePath2
3751
+ });
3752
+ page = await context.newPage();
3753
+ } else {
3754
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3755
+ }
3756
+ } else {
3757
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3758
+ }
3514
3759
  }
3515
3760
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
3516
3761
  try {
@@ -4015,6 +4260,66 @@ var init_snapshot = __esm(() => {
4015
4260
  ];
4016
4261
  });
4017
4262
 
4263
+ // src/lib/self-heal.ts
4264
+ async function healSelector(page, selector, sessionId) {
4265
+ const attempts = [];
4266
+ attempts.push(`selector: ${selector}`);
4267
+ try {
4268
+ const loc = page.locator(selector).first();
4269
+ if (await loc.count() > 0) {
4270
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
4271
+ }
4272
+ } catch {}
4273
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
4274
+ attempts.push(`text: "${selector}"`);
4275
+ try {
4276
+ const loc = page.getByText(selector, { exact: false }).first();
4277
+ if (await loc.count() > 0) {
4278
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
4279
+ }
4280
+ } catch {}
4281
+ }
4282
+ const roleMap = {
4283
+ button: ["button", "submit", "reset"],
4284
+ link: ["a"],
4285
+ input: ["input", "textarea"],
4286
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
4287
+ };
4288
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
4289
+ for (const [role, tags] of Object.entries(roleMap)) {
4290
+ attempts.push(`role: ${role} name~="${nameHint}"`);
4291
+ try {
4292
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
4293
+ if (await loc.count() > 0) {
4294
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
4295
+ }
4296
+ } catch {}
4297
+ }
4298
+ if (selector.startsWith("#")) {
4299
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
4300
+ const partialSel = `[id*="${idPart}"]`;
4301
+ attempts.push(`partial_id: ${partialSel}`);
4302
+ try {
4303
+ const loc = page.locator(partialSel).first();
4304
+ if (await loc.count() > 0) {
4305
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
4306
+ }
4307
+ } catch {}
4308
+ }
4309
+ if (selector.startsWith(".")) {
4310
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
4311
+ const partialSel = `[class*="${classPart}"]`;
4312
+ attempts.push(`partial_class: ${partialSel}`);
4313
+ try {
4314
+ const loc = page.locator(partialSel).first();
4315
+ if (await loc.count() > 0) {
4316
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
4317
+ }
4318
+ } catch {}
4319
+ }
4320
+ return { found: false, locator: null, method: "none", healed: false, attempts };
4321
+ }
4322
+
4018
4323
  // src/lib/actions.ts
4019
4324
  var exports_actions = {};
4020
4325
  __export(exports_actions, {
@@ -4056,11 +4361,22 @@ async function click(page, selector, opts) {
4056
4361
  delay: opts?.delay,
4057
4362
  timeout: opts?.timeout ?? 1e4
4058
4363
  });
4059
- } catch (err) {
4060
- if (err instanceof Error && err.message.includes("not found")) {
4364
+ return {};
4365
+ } catch (originalError) {
4366
+ if (opts?.selfHeal !== false) {
4367
+ const result = await healSelector(page, selector);
4368
+ if (result.found && result.locator) {
4369
+ await result.locator.click({
4370
+ button: opts?.button ?? "left",
4371
+ timeout: opts?.timeout ?? 1e4
4372
+ });
4373
+ return { healed: true, method: result.method, attempts: result.attempts };
4374
+ }
4375
+ }
4376
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
4061
4377
  throw new ElementNotFoundError(selector);
4062
4378
  }
4063
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
4379
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
4064
4380
  }
4065
4381
  }
4066
4382
  async function type(page, selector, text, opts) {
@@ -4069,17 +4385,35 @@ async function type(page, selector, text, opts) {
4069
4385
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
4070
4386
  }
4071
4387
  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")) {
4388
+ return {};
4389
+ } catch (originalError) {
4390
+ if (opts?.selfHeal !== false) {
4391
+ const result = await healSelector(page, selector);
4392
+ if (result.found && result.locator) {
4393
+ if (opts?.clear)
4394
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
4395
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
4396
+ return { healed: true, method: result.method, attempts: result.attempts };
4397
+ }
4398
+ }
4399
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
4074
4400
  throw new ElementNotFoundError(selector);
4075
4401
  }
4076
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
4402
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
4077
4403
  }
4078
4404
  }
4079
- async function fill(page, selector, value, timeout = 1e4) {
4405
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
4080
4406
  try {
4081
4407
  await page.fill(selector, value, { timeout });
4082
- } catch (err) {
4408
+ return {};
4409
+ } catch (originalError) {
4410
+ if (selfHeal) {
4411
+ const result = await healSelector(page, selector);
4412
+ if (result.found && result.locator) {
4413
+ await result.locator.fill(value, { timeout });
4414
+ return { healed: true, method: result.method, attempts: result.attempts };
4415
+ }
4416
+ }
4083
4417
  throw new ElementNotFoundError(selector);
4084
4418
  }
4085
4419
  }
@@ -4204,12 +4538,39 @@ async function clickText(page, text, opts) {
4204
4538
  }
4205
4539
  }, { retries: opts?.retries ?? 1 });
4206
4540
  }
4207
- async function fillForm(page, fields, submitSelector) {
4541
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
4208
4542
  let filled = 0;
4209
4543
  const errors = [];
4544
+ const healedFields = [];
4210
4545
  for (const [selector, value] of Object.entries(fields)) {
4211
4546
  try {
4212
- const el = await page.$(selector);
4547
+ let el = await page.$(selector);
4548
+ if (!el && selfHeal) {
4549
+ const result = await healSelector(page, selector);
4550
+ if (result.found && result.locator) {
4551
+ const handle = await result.locator.elementHandle();
4552
+ if (handle) {
4553
+ el = handle;
4554
+ healedFields.push(selector);
4555
+ const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
4556
+ const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
4557
+ if (tagName2 === "select") {
4558
+ await result.locator.selectOption(String(value));
4559
+ } else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
4560
+ if (Boolean(value))
4561
+ await result.locator.check();
4562
+ else
4563
+ await result.locator.uncheck();
4564
+ } else {
4565
+ await result.locator.fill(String(value));
4566
+ }
4567
+ filled++;
4568
+ continue;
4569
+ }
4570
+ }
4571
+ errors.push(`${selector}: element not found`);
4572
+ continue;
4573
+ }
4213
4574
  if (!el) {
4214
4575
  errors.push(`${selector}: element not found`);
4215
4576
  continue;
@@ -4236,11 +4597,21 @@ async function fillForm(page, fields, submitSelector) {
4236
4597
  if (submitSelector) {
4237
4598
  try {
4238
4599
  await page.click(submitSelector);
4239
- } catch (err) {
4240
- errors.push(`submit(${submitSelector}): ${err instanceof Error ? err.message : String(err)}`);
4600
+ } catch (submitErr) {
4601
+ if (selfHeal) {
4602
+ const result = await healSelector(page, submitSelector);
4603
+ if (result.found && result.locator) {
4604
+ await result.locator.click();
4605
+ healedFields.push(submitSelector);
4606
+ } else {
4607
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
4608
+ }
4609
+ } else {
4610
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
4611
+ }
4241
4612
  }
4242
4613
  }
4243
- return { filled, errors, fields_attempted: Object.keys(fields).length };
4614
+ return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
4244
4615
  }
4245
4616
  async function waitForText(page, text, opts) {
4246
4617
  const timeout = opts?.timeout ?? 1e4;
@@ -11096,17 +11467,17 @@ var init_gallery = __esm(() => {
11096
11467
  });
11097
11468
 
11098
11469
  // 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";
11470
+ import { join as join4 } from "path";
11471
+ import { mkdirSync as mkdirSync4 } from "fs";
11472
+ import { homedir as homedir4 } from "os";
11102
11473
  function getDataDir2() {
11103
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
11474
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
11104
11475
  }
11105
11476
  function getScreenshotDir(projectId) {
11106
- const base = join3(getDataDir2(), "screenshots");
11477
+ const base = join4(getDataDir2(), "screenshots");
11107
11478
  const date = new Date().toISOString().split("T")[0];
11108
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
11109
- mkdirSync3(dir, { recursive: true });
11479
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
11480
+ mkdirSync4(dir, { recursive: true });
11110
11481
  return dir;
11111
11482
  }
11112
11483
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -11121,7 +11492,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
11121
11492
  }
11122
11493
  }
11123
11494
  async function generateThumbnail(raw, dir, stem) {
11124
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
11495
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
11125
11496
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
11126
11497
  await Bun.write(thumbPath, thumbBuffer);
11127
11498
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -11178,7 +11549,7 @@ async function takeScreenshot(page, opts) {
11178
11549
  const compressedSizeBytes = finalBuffer.length;
11179
11550
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
11180
11551
  const ext = format;
11181
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
11552
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
11182
11553
  await Bun.write(screenshotPath, finalBuffer);
11183
11554
  let thumbnailPath;
11184
11555
  let thumbnailBase64;
@@ -11238,12 +11609,12 @@ async function takeScreenshot(page, opts) {
11238
11609
  }
11239
11610
  async function generatePDF(page, opts) {
11240
11611
  try {
11241
- const base = join3(getDataDir2(), "pdfs");
11612
+ const base = join4(getDataDir2(), "pdfs");
11242
11613
  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 });
11614
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
11615
+ mkdirSync4(dir, { recursive: true });
11245
11616
  const timestamp = Date.now();
11246
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
11617
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
11247
11618
  const buffer = await page.pdf({
11248
11619
  path: pdfPath,
11249
11620
  format: opts?.format ?? "A4",
@@ -11570,6 +11941,17 @@ var init_recordings = __esm(() => {
11570
11941
  });
11571
11942
 
11572
11943
  // src/lib/recorder.ts
11944
+ var exports_recorder = {};
11945
+ __export(exports_recorder, {
11946
+ stopRecording: () => stopRecording,
11947
+ startRecording: () => startRecording,
11948
+ replayRecording: () => replayRecording,
11949
+ recordStep: () => recordStep,
11950
+ listRecordings: () => listRecordings,
11951
+ getRecording: () => getRecording,
11952
+ exportRecording: () => exportRecording,
11953
+ attachPageListeners: () => attachPageListeners
11954
+ });
11573
11955
  function startRecording(sessionId, name, startUrl) {
11574
11956
  const steps = [];
11575
11957
  const recording = createRecording({ name, start_url: startUrl, steps });
@@ -11580,6 +11962,23 @@ function startRecording(sessionId, name, startUrl) {
11580
11962
  });
11581
11963
  return recording;
11582
11964
  }
11965
+ function attachPageListeners(page, recordingId) {
11966
+ const active = activeRecordings.get(recordingId);
11967
+ if (!active)
11968
+ throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
11969
+ const onFrameNav = () => {
11970
+ active.steps.push({
11971
+ type: "navigate",
11972
+ url: page.url(),
11973
+ timestamp: Date.now()
11974
+ });
11975
+ };
11976
+ page.on("framenavigated", onFrameNav);
11977
+ const cleanup = () => {
11978
+ page.off("framenavigated", onFrameNav);
11979
+ };
11980
+ active.cleanup = cleanup;
11981
+ }
11583
11982
  function recordStep(recordingId, step) {
11584
11983
  const active = activeRecordings.get(recordingId);
11585
11984
  if (!active)
@@ -11651,6 +12050,64 @@ async function replayRecording(recordingId, page) {
11651
12050
  duration_ms: Date.now() - startTime
11652
12051
  };
11653
12052
  }
12053
+ function exportRecording(recordingId, format = "json") {
12054
+ const recording = getRecording(recordingId);
12055
+ if (format === "json") {
12056
+ return JSON.stringify(recording, null, 2);
12057
+ }
12058
+ if (format === "playwright") {
12059
+ const lines2 = [
12060
+ `import { test, expect } from '@playwright/test';`,
12061
+ ``,
12062
+ `test('${recording.name}', async ({ page }) => {`
12063
+ ];
12064
+ for (const step of recording.steps) {
12065
+ switch (step.type) {
12066
+ case "navigate":
12067
+ lines2.push(` await page.goto('${step.url}');`);
12068
+ break;
12069
+ case "click":
12070
+ lines2.push(` await page.click('${step.selector}');`);
12071
+ break;
12072
+ case "type":
12073
+ lines2.push(` await page.type('${step.selector}', '${step.value}');`);
12074
+ break;
12075
+ case "scroll":
12076
+ lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
12077
+ break;
12078
+ case "evaluate":
12079
+ lines2.push(` await page.evaluate(${step.value});`);
12080
+ break;
12081
+ }
12082
+ }
12083
+ lines2.push(`});`);
12084
+ return lines2.join(`
12085
+ `);
12086
+ }
12087
+ const lines = [
12088
+ `const puppeteer = require('puppeteer');`,
12089
+ ``,
12090
+ `(async () => {`,
12091
+ ` const browser = await puppeteer.launch();`,
12092
+ ` const page = await browser.newPage();`
12093
+ ];
12094
+ for (const step of recording.steps) {
12095
+ switch (step.type) {
12096
+ case "navigate":
12097
+ lines.push(` await page.goto('${step.url}');`);
12098
+ break;
12099
+ case "click":
12100
+ lines.push(` await page.click('${step.selector}');`);
12101
+ break;
12102
+ case "type":
12103
+ lines.push(` await page.type('${step.selector}', '${step.value}');`);
12104
+ break;
12105
+ }
12106
+ }
12107
+ lines.push(` await browser.close();`, `})();`);
12108
+ return lines.join(`
12109
+ `);
12110
+ }
11654
12111
  var activeRecordings;
11655
12112
  var init_recorder = __esm(() => {
11656
12113
  init_recordings();
@@ -15625,118 +16082,6 @@ var init_zod = __esm(() => {
15625
16082
  init_external();
15626
16083
  });
15627
16084
 
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
16085
  // src/lib/performance.ts
15741
16086
  async function getPerformanceMetrics(page) {
15742
16087
  const navTiming = await page.evaluate(() => {
@@ -15842,16 +16187,16 @@ __export(exports_downloads, {
15842
16187
  cleanStaleDownloads: () => cleanStaleDownloads
15843
16188
  });
15844
16189
  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";
16190
+ import { join as join5, basename, extname } from "path";
16191
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
16192
+ import { homedir as homedir5 } from "os";
15848
16193
  function getDataDir3() {
15849
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
16194
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15850
16195
  }
15851
16196
  function getDownloadsDir(sessionId) {
15852
- const base = join4(getDataDir3(), "downloads");
15853
- const dir = sessionId ? join4(base, sessionId) : base;
15854
- mkdirSync4(dir, { recursive: true });
16197
+ const base = join5(getDataDir3(), "downloads");
16198
+ const dir = sessionId ? join5(base, sessionId) : base;
16199
+ mkdirSync5(dir, { recursive: true });
15855
16200
  return dir;
15856
16201
  }
15857
16202
  function ensureDownloadsDir() {
@@ -15866,7 +16211,7 @@ function saveToDownloads(buffer, filename, opts) {
15866
16211
  const ext = extname(filename) || "";
15867
16212
  const stem = basename(filename, ext);
15868
16213
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
15869
- const filePath = join4(dir, uniqueName);
16214
+ const filePath = join5(dir, uniqueName);
15870
16215
  writeFileSync(filePath, buffer);
15871
16216
  const meta = {
15872
16217
  id,
@@ -15895,20 +16240,20 @@ function listDownloads(sessionId) {
15895
16240
  const dir = getDownloadsDir(sessionId);
15896
16241
  const results = [];
15897
16242
  function scanDir(d) {
15898
- if (!existsSync(d))
16243
+ if (!existsSync2(d))
15899
16244
  return;
15900
- const entries = readdirSync(d);
16245
+ const entries = readdirSync2(d);
15901
16246
  for (const entry of entries) {
15902
16247
  if (entry.endsWith(".meta.json"))
15903
16248
  continue;
15904
- const full = join4(d, entry);
16249
+ const full = join5(d, entry);
15905
16250
  const stat = statSync(full);
15906
16251
  if (stat.isDirectory()) {
15907
16252
  scanDir(full);
15908
16253
  continue;
15909
16254
  }
15910
16255
  const mpath = metaPath(full);
15911
- if (!existsSync(mpath))
16256
+ if (!existsSync2(mpath))
15912
16257
  continue;
15913
16258
  try {
15914
16259
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -15938,9 +16283,9 @@ function deleteDownload(id, sessionId) {
15938
16283
  if (!file)
15939
16284
  return false;
15940
16285
  try {
15941
- unlinkSync(file.path);
15942
- if (existsSync(file.meta_path))
15943
- unlinkSync(file.meta_path);
16286
+ unlinkSync2(file.path);
16287
+ if (existsSync2(file.meta_path))
16288
+ unlinkSync2(file.meta_path);
15944
16289
  return true;
15945
16290
  } catch {
15946
16291
  return false;
@@ -15990,9 +16335,9 @@ var exports_gallery_diff = {};
15990
16335
  __export(exports_gallery_diff, {
15991
16336
  diffImages: () => diffImages
15992
16337
  });
15993
- import { join as join5 } from "path";
15994
- import { mkdirSync as mkdirSync5 } from "fs";
15995
- import { homedir as homedir5 } from "os";
16338
+ import { join as join6 } from "path";
16339
+ import { mkdirSync as mkdirSync6 } from "fs";
16340
+ import { homedir as homedir6 } from "os";
15996
16341
  async function diffImages(path1, path2) {
15997
16342
  const img1 = import_sharp2.default(path1);
15998
16343
  const img2 = import_sharp2.default(path2);
@@ -16023,10 +16368,10 @@ async function diffImages(path1, path2) {
16023
16368
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
16024
16369
  }
16025
16370
  }
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`);
16371
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16372
+ const diffDir = join6(dataDir, "diffs");
16373
+ mkdirSync6(diffDir, { recursive: true });
16374
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
16030
16375
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
16031
16376
  await Bun.write(diffPath, diffImageBuffer);
16032
16377
  return {
@@ -16043,9 +16388,9 @@ var init_gallery_diff = __esm(() => {
16043
16388
  });
16044
16389
 
16045
16390
  // 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";
16391
+ import { join as join7 } from "path";
16392
+ import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
16393
+ import { homedir as homedir7 } from "os";
16049
16394
  async function persistFile(localPath, opts) {
16050
16395
  try {
16051
16396
  const mod = await import("@hasna/files");
@@ -16054,12 +16399,12 @@ async function persistFile(localPath, opts) {
16054
16399
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
16055
16400
  }
16056
16401
  } catch {}
16057
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16402
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
16058
16403
  const date = new Date().toISOString().split("T")[0];
16059
- const dir = join6(dataDir, "persistent", date);
16060
- mkdirSync6(dir, { recursive: true });
16404
+ const dir = join7(dataDir, "persistent", date);
16405
+ mkdirSync7(dir, { recursive: true });
16061
16406
  const filename = localPath.split("/").pop() ?? "file";
16062
- const targetPath = join6(dir, filename);
16407
+ const targetPath = join7(dir, filename);
16063
16408
  copyFileSync2(localPath, targetPath);
16064
16409
  return {
16065
16410
  id: `local-${Date.now()}`,
@@ -16174,23 +16519,23 @@ __export(exports_profiles, {
16174
16519
  deleteProfile: () => deleteProfile,
16175
16520
  applyProfile: () => applyProfile
16176
16521
  });
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";
16522
+ import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
16523
+ import { join as join8 } from "path";
16524
+ import { homedir as homedir8 } from "os";
16180
16525
  function getProfilesDir() {
16181
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
16182
- const dir = join7(dataDir, "profiles");
16183
- mkdirSync7(dir, { recursive: true });
16526
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
16527
+ const dir = join8(dataDir, "profiles");
16528
+ mkdirSync8(dir, { recursive: true });
16184
16529
  return dir;
16185
16530
  }
16186
16531
  function getProfileDir2(name) {
16187
- return join7(getProfilesDir(), name);
16532
+ return join8(getProfilesDir(), name);
16188
16533
  }
16189
16534
  async function saveProfile(page, name) {
16190
16535
  const dir = getProfileDir2(name);
16191
- mkdirSync7(dir, { recursive: true });
16536
+ mkdirSync8(dir, { recursive: true });
16192
16537
  const cookies = await page.context().cookies();
16193
- writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16538
+ writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16194
16539
  let localStorage2 = {};
16195
16540
  try {
16196
16541
  localStorage2 = await page.evaluate(() => {
@@ -16202,11 +16547,11 @@ async function saveProfile(page, name) {
16202
16547
  return result;
16203
16548
  });
16204
16549
  } catch {}
16205
- writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16550
+ writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16206
16551
  const savedAt = new Date().toISOString();
16207
16552
  const url = page.url();
16208
16553
  const meta = { saved_at: savedAt, url };
16209
- writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
16554
+ writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
16210
16555
  return {
16211
16556
  name,
16212
16557
  saved_at: savedAt,
@@ -16217,17 +16562,17 @@ async function saveProfile(page, name) {
16217
16562
  }
16218
16563
  function loadProfile(name) {
16219
16564
  const dir = getProfileDir2(name);
16220
- if (!existsSync3(dir)) {
16565
+ if (!existsSync4(dir)) {
16221
16566
  throw new Error(`Profile not found: ${name}`);
16222
16567
  }
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")) : {};
16568
+ const cookiesPath = join8(dir, "cookies.json");
16569
+ const storagePath = join8(dir, "storage.json");
16570
+ const metaPath2 = join8(dir, "meta.json");
16571
+ const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
16572
+ const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
16228
16573
  let savedAt = new Date().toISOString();
16229
16574
  let url;
16230
- if (existsSync3(metaPath2)) {
16575
+ if (existsSync4(metaPath2)) {
16231
16576
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16232
16577
  savedAt = meta.saved_at ?? savedAt;
16233
16578
  url = meta.url;
@@ -16255,33 +16600,33 @@ async function applyProfile(page, profileData) {
16255
16600
  }
16256
16601
  function listProfiles() {
16257
16602
  const dir = getProfilesDir();
16258
- if (!existsSync3(dir))
16603
+ if (!existsSync4(dir))
16259
16604
  return [];
16260
- const entries = readdirSync2(dir, { withFileTypes: true });
16605
+ const entries = readdirSync3(dir, { withFileTypes: true });
16261
16606
  const profiles = [];
16262
16607
  for (const entry of entries) {
16263
16608
  if (!entry.isDirectory())
16264
16609
  continue;
16265
16610
  const name = entry.name;
16266
- const profileDir = join7(dir, name);
16611
+ const profileDir = join8(dir, name);
16267
16612
  let savedAt = "";
16268
16613
  let url;
16269
16614
  let cookieCount = 0;
16270
16615
  let storageKeyCount = 0;
16271
16616
  try {
16272
- const metaPath2 = join7(profileDir, "meta.json");
16273
- if (existsSync3(metaPath2)) {
16617
+ const metaPath2 = join8(profileDir, "meta.json");
16618
+ if (existsSync4(metaPath2)) {
16274
16619
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16275
16620
  savedAt = meta.saved_at ?? "";
16276
16621
  url = meta.url;
16277
16622
  }
16278
- const cookiesPath = join7(profileDir, "cookies.json");
16279
- if (existsSync3(cookiesPath)) {
16623
+ const cookiesPath = join8(profileDir, "cookies.json");
16624
+ if (existsSync4(cookiesPath)) {
16280
16625
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
16281
16626
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
16282
16627
  }
16283
- const storagePath = join7(profileDir, "storage.json");
16284
- if (existsSync3(storagePath)) {
16628
+ const storagePath = join8(profileDir, "storage.json");
16629
+ if (existsSync4(storagePath)) {
16285
16630
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
16286
16631
  storageKeyCount = Object.keys(storage).length;
16287
16632
  }
@@ -16298,7 +16643,7 @@ function listProfiles() {
16298
16643
  }
16299
16644
  function deleteProfile(name) {
16300
16645
  const dir = getProfileDir2(name);
16301
- if (!existsSync3(dir))
16646
+ if (!existsSync4(dir))
16302
16647
  return false;
16303
16648
  try {
16304
16649
  rmSync(dir, { recursive: true, force: true });
@@ -16309,6 +16654,97 @@ function deleteProfile(name) {
16309
16654
  }
16310
16655
  var init_profiles = () => {};
16311
16656
 
16657
+ // src/lib/sanitize.ts
16658
+ var exports_sanitize = {};
16659
+ __export(exports_sanitize, {
16660
+ sanitizeText: () => sanitizeText,
16661
+ sanitizeHTML: () => sanitizeHTML
16662
+ });
16663
+ function sanitizeText(text) {
16664
+ let stripped = 0;
16665
+ const warnings = [];
16666
+ let clean = text;
16667
+ for (const pattern of INJECTION_PATTERNS) {
16668
+ pattern.lastIndex = 0;
16669
+ const matches = clean.match(pattern);
16670
+ if (matches) {
16671
+ stripped += matches.length;
16672
+ warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
16673
+ pattern.lastIndex = 0;
16674
+ clean = clean.replace(pattern, "[STRIPPED]");
16675
+ }
16676
+ }
16677
+ return { text: clean, stripped, warnings };
16678
+ }
16679
+ function sanitizeHTML(html) {
16680
+ let stripped = 0;
16681
+ const warnings = [];
16682
+ let clean = html;
16683
+ const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
16684
+ if (commentMatches) {
16685
+ for (const comment of commentMatches) {
16686
+ if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
16687
+ stripped++;
16688
+ warnings.push(`Stripped HTML comment (${comment.length} chars)`);
16689
+ }
16690
+ }
16691
+ clean = clean.replace(/<!--[\s\S]*?-->/g, "");
16692
+ }
16693
+ const hiddenPatterns = [
16694
+ /style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
16695
+ /style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
16696
+ /style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
16697
+ /style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
16698
+ /style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
16699
+ ];
16700
+ for (const pattern of hiddenPatterns) {
16701
+ pattern.lastIndex = 0;
16702
+ const matches = clean.match(pattern);
16703
+ if (matches) {
16704
+ stripped += matches.length;
16705
+ warnings.push(`Stripped ${matches.length} hidden elements`);
16706
+ pattern.lastIndex = 0;
16707
+ clean = clean.replace(pattern, "");
16708
+ }
16709
+ }
16710
+ const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
16711
+ const ariaHidden = clean.match(ariaHiddenPattern);
16712
+ if (ariaHidden) {
16713
+ stripped += ariaHidden.length;
16714
+ warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
16715
+ ariaHiddenPattern.lastIndex = 0;
16716
+ clean = clean.replace(ariaHiddenPattern, "");
16717
+ }
16718
+ const textResult = sanitizeText(clean);
16719
+ return {
16720
+ text: textResult.text,
16721
+ stripped: stripped + textResult.stripped,
16722
+ warnings: [...warnings, ...textResult.warnings]
16723
+ };
16724
+ }
16725
+ var INJECTION_PATTERNS;
16726
+ var init_sanitize = __esm(() => {
16727
+ INJECTION_PATTERNS = [
16728
+ /ignore\s+(all\s+)?previous\s+instructions/gi,
16729
+ /ignore\s+(all\s+)?prior\s+instructions/gi,
16730
+ /disregard\s+(all\s+)?previous/gi,
16731
+ /forget\s+(all\s+)?previous/gi,
16732
+ /you\s+are\s+now\s+/gi,
16733
+ /new\s+instructions?\s*:/gi,
16734
+ /system\s+prompt\s*:/gi,
16735
+ /\[INST\]/gi,
16736
+ /\[\/INST\]/gi,
16737
+ /<\|im_start\|>/gi,
16738
+ /<\|im_end\|>/gi,
16739
+ /<<SYS>>/gi,
16740
+ /<<\/SYS>>/gi,
16741
+ /IMPORTANT:\s*ignore/gi,
16742
+ /CRITICAL:\s*override/gi,
16743
+ /assistant:\s/gi,
16744
+ /human:\s/gi
16745
+ ];
16746
+ });
16747
+
16312
16748
  // src/lib/annotate.ts
16313
16749
  var exports_annotate = {};
16314
16750
  __export(exports_annotate, {
@@ -16369,26 +16805,213 @@ var init_annotate = __esm(() => {
16369
16805
  import_sharp3 = __toESM(require_lib(), 1);
16370
16806
  });
16371
16807
 
16808
+ // src/lib/auth-flow.ts
16809
+ var exports_auth_flow = {};
16810
+ __export(exports_auth_flow, {
16811
+ tryReplayAuth: () => tryReplayAuth,
16812
+ touchAuthFlow: () => touchAuthFlow,
16813
+ saveAuthFlow: () => saveAuthFlow,
16814
+ listAuthFlows: () => listAuthFlows,
16815
+ isAuthRedirect: () => isAuthRedirect,
16816
+ isAuthPage: () => isAuthPage,
16817
+ getAuthFlowByName: () => getAuthFlowByName,
16818
+ getAuthFlowByDomain: () => getAuthFlowByDomain,
16819
+ getAuthFlow: () => getAuthFlow,
16820
+ deleteAuthFlow: () => deleteAuthFlow
16821
+ });
16822
+ import { randomUUID as randomUUID10 } from "crypto";
16823
+ function saveAuthFlow(data) {
16824
+ const db = getDatabase();
16825
+ const id = randomUUID10();
16826
+ 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);
16827
+ return getAuthFlow(id);
16828
+ }
16829
+ function getAuthFlow(id) {
16830
+ const db = getDatabase();
16831
+ return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
16832
+ }
16833
+ function getAuthFlowByName(name) {
16834
+ const db = getDatabase();
16835
+ return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
16836
+ }
16837
+ function getAuthFlowByDomain(domain) {
16838
+ const db = getDatabase();
16839
+ return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
16840
+ }
16841
+ function listAuthFlows() {
16842
+ const db = getDatabase();
16843
+ return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
16844
+ }
16845
+ function deleteAuthFlow(name) {
16846
+ const db = getDatabase();
16847
+ const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
16848
+ return result.changes > 0;
16849
+ }
16850
+ function touchAuthFlow(id) {
16851
+ const db = getDatabase();
16852
+ db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
16853
+ }
16854
+ function isAuthPage(url) {
16855
+ return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
16856
+ }
16857
+ function isAuthRedirect(fromUrl, toUrl) {
16858
+ if (fromUrl === toUrl)
16859
+ return false;
16860
+ return isAuthPage(toUrl) && !isAuthPage(fromUrl);
16861
+ }
16862
+ async function tryReplayAuth(page, domain) {
16863
+ const flow = getAuthFlowByDomain(domain);
16864
+ if (!flow)
16865
+ return { replayed: false };
16866
+ if (flow.storage_state_path) {
16867
+ try {
16868
+ const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
16869
+ if (existsSync5(flow.storage_state_path)) {
16870
+ const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
16871
+ if (state.cookies?.length) {
16872
+ await page.context().addCookies(state.cookies);
16873
+ await page.reload();
16874
+ await new Promise((r) => setTimeout(r, 1000));
16875
+ if (!isAuthPage(page.url())) {
16876
+ touchAuthFlow(flow.id);
16877
+ return { replayed: true, flow, method: "storage_state" };
16878
+ }
16879
+ }
16880
+ }
16881
+ } catch {}
16882
+ }
16883
+ if (flow.recording_id) {
16884
+ try {
16885
+ const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
16886
+ const result = await replayRecording2(flow.recording_id, page);
16887
+ if (result.success) {
16888
+ try {
16889
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
16890
+ const path = await saveStateFromPage2(page, flow.name);
16891
+ const db = getDatabase();
16892
+ db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
16893
+ } catch {}
16894
+ touchAuthFlow(flow.id);
16895
+ return { replayed: true, flow, method: "recording_replay" };
16896
+ }
16897
+ } catch {}
16898
+ }
16899
+ return { replayed: false, flow };
16900
+ }
16901
+ var AUTH_URL_PATTERNS;
16902
+ var init_auth_flow = __esm(() => {
16903
+ init_schema();
16904
+ AUTH_URL_PATTERNS = [
16905
+ /\/login/i,
16906
+ /\/signin/i,
16907
+ /\/sign-in/i,
16908
+ /\/auth/i,
16909
+ /\/sso/i,
16910
+ /\/oauth/i,
16911
+ /\/cas\/login/i,
16912
+ /accounts\.google\.com/i,
16913
+ /login\.microsoftonline\.com/i,
16914
+ /github\.com\/login/i,
16915
+ /auth0\.com/i
16916
+ ];
16917
+ });
16918
+
16919
+ // src/lib/vision-fallback.ts
16920
+ var exports_vision_fallback = {};
16921
+ __export(exports_vision_fallback, {
16922
+ findElementByVision: () => findElementByVision,
16923
+ clickByVision: () => clickByVision
16924
+ });
16925
+ async function findElementByVision(page, description, opts) {
16926
+ const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
16927
+ const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
16928
+ const base64 = screenshot.toString("base64");
16929
+ const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
16930
+ const apiKey = process.env["ANTHROPIC_API_KEY"];
16931
+ if (!apiKey) {
16932
+ return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
16933
+ }
16934
+ try {
16935
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
16936
+ method: "POST",
16937
+ headers: {
16938
+ "content-type": "application/json",
16939
+ "x-api-key": apiKey,
16940
+ "anthropic-version": "2023-06-01"
16941
+ },
16942
+ body: JSON.stringify({
16943
+ model,
16944
+ max_tokens: 256,
16945
+ messages: [{
16946
+ role: "user",
16947
+ content: [
16948
+ {
16949
+ type: "image",
16950
+ source: { type: "base64", media_type: "image/jpeg", data: base64 }
16951
+ },
16952
+ {
16953
+ type: "text",
16954
+ text: `Find the element matching this description: "${description}"
16955
+
16956
+ The screenshot is ${viewport.width}x${viewport.height} pixels.
16957
+
16958
+ Reply with ONLY a JSON object (no markdown, no explanation):
16959
+ {"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
16960
+
16961
+ If you cannot find the element:
16962
+ {"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
16963
+ }
16964
+ ]
16965
+ }]
16966
+ })
16967
+ });
16968
+ const data = await response.json();
16969
+ const text = data.content?.[0]?.text ?? "";
16970
+ const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
16971
+ const result = JSON.parse(jsonStr);
16972
+ result.model = model;
16973
+ return result;
16974
+ } catch (err) {
16975
+ return {
16976
+ found: false,
16977
+ x: 0,
16978
+ y: 0,
16979
+ confidence: "none",
16980
+ description,
16981
+ model,
16982
+ error: err instanceof Error ? err.message : String(err)
16983
+ };
16984
+ }
16985
+ }
16986
+ async function clickByVision(page, description, opts) {
16987
+ const result = await findElementByVision(page, description, opts);
16988
+ if (result.found && result.x > 0 && result.y > 0) {
16989
+ await page.mouse.click(result.x, result.y);
16990
+ }
16991
+ return result;
16992
+ }
16993
+ var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
16994
+
16372
16995
  // src/lib/auth.ts
16373
16996
  var exports_auth = {};
16374
16997
  __export(exports_auth, {
16375
16998
  loginWithCredentials: () => loginWithCredentials,
16376
16999
  getCredentials: () => getCredentials
16377
17000
  });
16378
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
16379
- import { join as join8 } from "path";
16380
- import { homedir as homedir8 } from "os";
17001
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
17002
+ import { join as join9 } from "path";
17003
+ import { homedir as homedir9 } from "os";
16381
17004
  async function getCredentials(service) {
16382
17005
  try {
16383
- const { getSecret } = await import(`${homedir8()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
17006
+ const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
16384
17007
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
16385
17008
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
16386
17009
  if (email?.value && password?.value) {
16387
17010
  return { email: email.value, password: password.value };
16388
17011
  }
16389
17012
  } catch {}
16390
- const secretsPath = join8(homedir8(), ".secrets");
16391
- if (existsSync4(secretsPath)) {
17013
+ const secretsPath = join9(homedir9(), ".secrets");
17014
+ if (existsSync5(secretsPath)) {
16392
17015
  const content = readFileSync3(secretsPath, "utf8");
16393
17016
  const lines = content.split(`
16394
17017
  `);
@@ -16565,14 +17188,14 @@ __export(exports_dist, {
16565
17188
  InvalidScopeError: () => InvalidScopeError,
16566
17189
  EntityNotFoundError: () => EntityNotFoundError,
16567
17190
  DuplicateMemoryError: () => DuplicateMemoryError,
16568
- DEFAULT_MODEL: () => DEFAULT_MODEL,
17191
+ DEFAULT_MODEL: () => DEFAULT_MODEL2,
16569
17192
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
16570
17193
  });
16571
17194
  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";
17195
+ import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
17196
+ import { dirname, join as join10, resolve } from "path";
17197
+ import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
17198
+ import { homedir as homedir10 } from "os";
16576
17199
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
16577
17200
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
16578
17201
  import { homedir as homedir22 } from "os";
@@ -16586,8 +17209,8 @@ function isInMemoryDb(path) {
16586
17209
  function findNearestMementosDb(startDir) {
16587
17210
  let dir = resolve(startDir);
16588
17211
  while (true) {
16589
- const candidate = join9(dir, ".mementos", "mementos.db");
16590
- if (existsSync5(candidate))
17212
+ const candidate = join10(dir, ".mementos", "mementos.db");
17213
+ if (existsSync6(candidate))
16591
17214
  return candidate;
16592
17215
  const parent = dirname(dir);
16593
17216
  if (parent === dir)
@@ -16599,7 +17222,7 @@ function findNearestMementosDb(startDir) {
16599
17222
  function findGitRoot(startDir) {
16600
17223
  let dir = resolve(startDir);
16601
17224
  while (true) {
16602
- if (existsSync5(join9(dir, ".git")))
17225
+ if (existsSync6(join10(dir, ".git")))
16603
17226
  return dir;
16604
17227
  const parent = dirname(dir);
16605
17228
  if (parent === dir)
@@ -16619,25 +17242,25 @@ function getDbPath() {
16619
17242
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
16620
17243
  const gitRoot = findGitRoot(cwd);
16621
17244
  if (gitRoot) {
16622
- return join9(gitRoot, ".mementos", "mementos.db");
17245
+ return join10(gitRoot, ".mementos", "mementos.db");
16623
17246
  }
16624
17247
  }
16625
17248
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
16626
- return join9(home, ".mementos", "mementos.db");
17249
+ return join10(home, ".mementos", "mementos.db");
16627
17250
  }
16628
- function ensureDir(filePath) {
17251
+ function ensureDir2(filePath) {
16629
17252
  if (isInMemoryDb(filePath))
16630
17253
  return;
16631
17254
  const dir = dirname(resolve(filePath));
16632
- if (!existsSync5(dir)) {
16633
- mkdirSync8(dir, { recursive: true });
17255
+ if (!existsSync6(dir)) {
17256
+ mkdirSync9(dir, { recursive: true });
16634
17257
  }
16635
17258
  }
16636
17259
  function getDatabase2(dbPath) {
16637
17260
  if (_db2)
16638
17261
  return _db2;
16639
17262
  const path = dbPath || getDbPath();
16640
- ensureDir(path);
17263
+ ensureDir2(path);
16641
17264
  _db2 = new Database2(path, { create: true });
16642
17265
  _db2.run("PRAGMA journal_mode = WAL");
16643
17266
  _db2.run("PRAGMA busy_timeout = 5000");
@@ -18480,7 +19103,7 @@ function isValidCategory(value) {
18480
19103
  return VALID_CATEGORIES.includes(value);
18481
19104
  }
18482
19105
  function loadConfig() {
18483
- const configPath = join22(homedir9(), ".mementos", "config.json");
19106
+ const configPath = join22(homedir10(), ".mementos", "config.json");
18484
19107
  let fileConfig = {};
18485
19108
  if (existsSync22(configPath)) {
18486
19109
  try {
@@ -18507,10 +19130,10 @@ function loadConfig() {
18507
19130
  return merged;
18508
19131
  }
18509
19132
  function profilesDir() {
18510
- return join22(homedir9(), ".mementos", "profiles");
19133
+ return join22(homedir10(), ".mementos", "profiles");
18511
19134
  }
18512
19135
  function globalConfigPath() {
18513
- return join22(homedir9(), ".mementos", "config.json");
19136
+ return join22(homedir10(), ".mementos", "config.json");
18514
19137
  }
18515
19138
  function readGlobalConfig() {
18516
19139
  const p = globalConfigPath();
@@ -18524,7 +19147,7 @@ function readGlobalConfig() {
18524
19147
  }
18525
19148
  function writeGlobalConfig(data) {
18526
19149
  const p = globalConfigPath();
18527
- ensureDir2(dirname2(p));
19150
+ ensureDir22(dirname2(p));
18528
19151
  writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
18529
19152
  }
18530
19153
  function getActiveProfile() {
@@ -18547,18 +19170,18 @@ function listProfiles2() {
18547
19170
  const dir = profilesDir();
18548
19171
  if (!existsSync22(dir))
18549
19172
  return [];
18550
- return readdirSync3(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
19173
+ return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
18551
19174
  }
18552
19175
  function deleteProfile2(name) {
18553
19176
  const dbPath = join22(profilesDir(), `${name}.db`);
18554
19177
  if (!existsSync22(dbPath))
18555
19178
  return false;
18556
- unlinkSync2(dbPath);
19179
+ unlinkSync3(dbPath);
18557
19180
  if (getActiveProfile() === name)
18558
19181
  setActiveProfile(null);
18559
19182
  return true;
18560
19183
  }
18561
- function ensureDir2(dir) {
19184
+ function ensureDir22(dir) {
18562
19185
  if (!existsSync22(dir)) {
18563
19186
  mkdirSync22(dir, { recursive: true });
18564
19187
  }
@@ -19680,7 +20303,7 @@ function writeConfig(config) {
19680
20303
  }
19681
20304
  function getActiveModel() {
19682
20305
  const config = readConfig();
19683
- return config.activeModel ?? DEFAULT_MODEL;
20306
+ return config.activeModel ?? DEFAULT_MODEL2;
19684
20307
  }
19685
20308
  function setActiveModel(modelId) {
19686
20309
  const config = readConfig();
@@ -19754,7 +20377,7 @@ Return JSON with this exact shape:
19754
20377
  examples: finalExamples,
19755
20378
  count: finalExamples.length
19756
20379
  };
19757
- }, DEFAULT_MODEL = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
20380
+ }, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
19758
20381
  var init_dist = __esm(() => {
19759
20382
  __defProp2 = Object.defineProperty;
19760
20383
  exports_database = {};
@@ -21190,10 +21813,10 @@ __export(exports_dist2, {
21190
21813
  acquireLock: () => acquireLock2
21191
21814
  });
21192
21815
  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";
21816
+ import { mkdirSync as mkdirSync10 } from "fs";
21817
+ import { join as join11, dirname as dirname3 } from "path";
21818
+ import { homedir as homedir11 } from "os";
21819
+ import { randomUUID as randomUUID11 } from "crypto";
21197
21820
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
21198
21821
  import { join as join33 } from "path";
21199
21822
  import { homedir as homedir33 } from "os";
@@ -21207,13 +21830,13 @@ import { homedir as homedir42 } from "os";
21207
21830
  function getDbPath2() {
21208
21831
  if (process.env.CONVERSATIONS_DB_PATH)
21209
21832
  return process.env.CONVERSATIONS_DB_PATH;
21210
- return join10(homedir10(), ".conversations", "messages.db");
21833
+ return join11(homedir11(), ".conversations", "messages.db");
21211
21834
  }
21212
21835
  function getDb() {
21213
21836
  if (db)
21214
21837
  return db;
21215
21838
  const dbPath = getDbPath2();
21216
- mkdirSync9(dirname3(dbPath), { recursive: true });
21839
+ mkdirSync10(dirname3(dbPath), { recursive: true });
21217
21840
  db = new Database3(dbPath, { create: true });
21218
21841
  db.exec("PRAGMA journal_mode = WAL");
21219
21842
  db.exec("PRAGMA busy_timeout = 5000");
@@ -21573,7 +22196,7 @@ function guessMimeType(name) {
21573
22196
  function sendMessage(opts) {
21574
22197
  const db2 = getDb();
21575
22198
  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)}`);
22199
+ const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
21577
22200
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
21578
22201
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
21579
22202
  const blocking = opts.blocking ? 1 : 0;
@@ -25615,11 +26238,11 @@ __export(exports_dist3, {
25615
26238
  AgentNotFoundError: () => AgentNotFoundError2
25616
26239
  });
25617
26240
  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";
26241
+ import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
26242
+ import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
25620
26243
  import { existsSync as existsSync33 } from "fs";
25621
26244
  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";
26245
+ import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
25623
26246
  import { join as join24 } from "path";
25624
26247
  import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
25625
26248
  import { join as join44 } from "path";
@@ -25849,8 +26472,8 @@ function isInMemoryDb2(path) {
25849
26472
  function findNearestTodosDb(startDir) {
25850
26473
  let dir = resolve3(startDir);
25851
26474
  while (true) {
25852
- const candidate = join11(dir, ".todos", "todos.db");
25853
- if (existsSync6(candidate))
26475
+ const candidate = join12(dir, ".todos", "todos.db");
26476
+ if (existsSync7(candidate))
25854
26477
  return candidate;
25855
26478
  const parent = dirname5(dir);
25856
26479
  if (parent === dir)
@@ -25862,7 +26485,7 @@ function findNearestTodosDb(startDir) {
25862
26485
  function findGitRoot2(startDir) {
25863
26486
  let dir = resolve3(startDir);
25864
26487
  while (true) {
25865
- if (existsSync6(join11(dir, ".git")))
26488
+ if (existsSync7(join12(dir, ".git")))
25866
26489
  return dir;
25867
26490
  const parent = dirname5(dir);
25868
26491
  if (parent === dir)
@@ -25882,18 +26505,18 @@ function getDbPath3() {
25882
26505
  if (process.env["TODOS_DB_SCOPE"] === "project") {
25883
26506
  const gitRoot = findGitRoot2(cwd);
25884
26507
  if (gitRoot) {
25885
- return join11(gitRoot, ".todos", "todos.db");
26508
+ return join12(gitRoot, ".todos", "todos.db");
25886
26509
  }
25887
26510
  }
25888
26511
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
25889
- return join11(home, ".todos", "todos.db");
26512
+ return join12(home, ".todos", "todos.db");
25890
26513
  }
25891
26514
  function ensureDir3(filePath) {
25892
26515
  if (isInMemoryDb2(filePath))
25893
26516
  return;
25894
26517
  const dir = dirname5(resolve3(filePath));
25895
- if (!existsSync6(dir)) {
25896
- mkdirSync10(dir, { recursive: true });
26518
+ if (!existsSync7(dir)) {
26519
+ mkdirSync11(dir, { recursive: true });
25897
26520
  }
25898
26521
  }
25899
26522
  function getDatabase3(dbPath) {
@@ -26352,14 +26975,14 @@ function ensureProject2(name, path, db2) {
26352
26975
  }
26353
26976
  return createProject3({ name, path }, d);
26354
26977
  }
26355
- function ensureDir22(dir) {
26978
+ function ensureDir23(dir) {
26356
26979
  if (!existsSync23(dir))
26357
26980
  mkdirSync24(dir, { recursive: true });
26358
26981
  }
26359
26982
  function listJsonFiles(dir) {
26360
26983
  if (!existsSync23(dir))
26361
26984
  return [];
26362
- return readdirSync4(dir).filter((f) => f.endsWith(".json"));
26985
+ return readdirSync5(dir).filter((f) => f.endsWith(".json"));
26363
26986
  }
26364
26987
  function readJsonFile(path) {
26365
26988
  try {
@@ -29224,7 +29847,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
29224
29847
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
29225
29848
  const dir = getTaskListDir(taskListId);
29226
29849
  if (!existsSync43(dir))
29227
- ensureDir22(dir);
29850
+ ensureDir23(dir);
29228
29851
  const filter = {};
29229
29852
  if (projectId)
29230
29853
  filter["project_id"] = projectId;
@@ -29442,7 +30065,7 @@ function metadataKey(agent) {
29442
30065
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
29443
30066
  const dir = getTaskListDir2(agent, taskListId);
29444
30067
  if (!existsSync52(dir))
29445
- ensureDir22(dir);
30068
+ ensureDir23(dir);
29446
30069
  const filter = {};
29447
30070
  if (projectId)
29448
30071
  filter["project_id"] = projectId;
@@ -30970,9 +31593,9 @@ __export(exports_dist4, {
30970
31593
  CATEGORIES: () => CATEGORIES,
30971
31594
  AGENT_TARGETS: () => AGENT_TARGETS
30972
31595
  });
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";
31596
+ import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
31597
+ import { join as join13, dirname as dirname6 } from "path";
31598
+ import { homedir as homedir12 } from "os";
30976
31599
  import { fileURLToPath } from "url";
30977
31600
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
30978
31601
  import { join as join25 } from "path";
@@ -31083,35 +31706,35 @@ function normalizeSkillName(name) {
31083
31706
  function findSkillsDir() {
31084
31707
  let dir = __dirname2;
31085
31708
  for (let i = 0;i < 5; i++) {
31086
- const candidate = join12(dir, "skills");
31087
- if (existsSync7(candidate)) {
31709
+ const candidate = join13(dir, "skills");
31710
+ if (existsSync8(candidate)) {
31088
31711
  return candidate;
31089
31712
  }
31090
31713
  dir = dirname6(dir);
31091
31714
  }
31092
- return join12(__dirname2, "..", "skills");
31715
+ return join13(__dirname2, "..", "skills");
31093
31716
  }
31094
31717
  function getSkillPath(name) {
31095
31718
  const skillName = normalizeSkillName(name);
31096
- return join12(SKILLS_DIR, skillName);
31719
+ return join13(SKILLS_DIR, skillName);
31097
31720
  }
31098
31721
  function skillExists(name) {
31099
- return existsSync7(getSkillPath(name));
31722
+ return existsSync8(getSkillPath(name));
31100
31723
  }
31101
31724
  function installSkill(name, options = {}) {
31102
31725
  const { targetDir = process.cwd(), overwrite = false } = options;
31103
31726
  const skillName = normalizeSkillName(name);
31104
31727
  const sourcePath = getSkillPath(name);
31105
- const destDir = join12(targetDir, ".skills");
31106
- const destPath = join12(destDir, skillName);
31107
- if (!existsSync7(sourcePath)) {
31728
+ const destDir = join13(targetDir, ".skills");
31729
+ const destPath = join13(destDir, skillName);
31730
+ if (!existsSync8(sourcePath)) {
31108
31731
  return {
31109
31732
  skill: name,
31110
31733
  success: false,
31111
31734
  error: `Skill '${name}' not found`
31112
31735
  };
31113
31736
  }
31114
- if (existsSync7(destPath) && !overwrite) {
31737
+ if (existsSync8(destPath) && !overwrite) {
31115
31738
  return {
31116
31739
  skill: name,
31117
31740
  success: false,
@@ -31120,10 +31743,10 @@ function installSkill(name, options = {}) {
31120
31743
  };
31121
31744
  }
31122
31745
  try {
31123
- if (!existsSync7(destDir)) {
31124
- mkdirSync11(destDir, { recursive: true });
31746
+ if (!existsSync8(destDir)) {
31747
+ mkdirSync12(destDir, { recursive: true });
31125
31748
  }
31126
- if (existsSync7(destPath) && overwrite) {
31749
+ if (existsSync8(destPath) && overwrite) {
31127
31750
  rmSync2(destPath, { recursive: true, force: true });
31128
31751
  }
31129
31752
  cpSync(sourcePath, destPath, {
@@ -31162,10 +31785,10 @@ function installSkills(names, options = {}) {
31162
31785
  return names.map((name) => installSkill(name, options));
31163
31786
  }
31164
31787
  function updateSkillsIndex(skillsDir) {
31165
- const indexPath = join12(skillsDir, "index.ts");
31788
+ const indexPath = join13(skillsDir, "index.ts");
31166
31789
  const meta = loadMeta(skillsDir);
31167
31790
  const disabledSet = new Set(meta.disabled || []);
31168
- const skills = readdirSync5(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
31791
+ const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
31169
31792
  const exports = skills.map((s) => {
31170
31793
  const name = s.replace("skill-", "").replace(/-/g, "_");
31171
31794
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -31181,11 +31804,11 @@ ${exports}
31181
31804
  writeFileSync6(indexPath, content);
31182
31805
  }
31183
31806
  function getMetaPath(skillsDir) {
31184
- return join12(skillsDir, ".meta.json");
31807
+ return join13(skillsDir, ".meta.json");
31185
31808
  }
31186
31809
  function loadMeta(skillsDir) {
31187
31810
  const metaPath2 = getMetaPath(skillsDir);
31188
- if (existsSync7(metaPath2)) {
31811
+ if (existsSync8(metaPath2)) {
31189
31812
  try {
31190
31813
  return JSON.parse(readFileSync7(metaPath2, "utf-8"));
31191
31814
  } catch {}
@@ -31200,8 +31823,8 @@ function recordInstall(skillsDir, name) {
31200
31823
  const skillName = normalizeSkillName(name);
31201
31824
  let version = "unknown";
31202
31825
  try {
31203
- const pkgPath = join12(skillsDir, skillName, "package.json");
31204
- if (existsSync7(pkgPath)) {
31826
+ const pkgPath = join13(skillsDir, skillName, "package.json");
31827
+ if (existsSync8(pkgPath)) {
31205
31828
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
31206
31829
  version = pkg.version || "unknown";
31207
31830
  }
@@ -31215,12 +31838,12 @@ function recordRemove(skillsDir, name) {
31215
31838
  saveMeta(skillsDir, meta);
31216
31839
  }
31217
31840
  function getInstallMeta(targetDir = process.cwd()) {
31218
- return loadMeta(join12(targetDir, ".skills"));
31841
+ return loadMeta(join13(targetDir, ".skills"));
31219
31842
  }
31220
31843
  function disableSkill(name, targetDir = process.cwd()) {
31221
- const skillsDir = join12(targetDir, ".skills");
31844
+ const skillsDir = join13(targetDir, ".skills");
31222
31845
  const skillName = normalizeSkillName(name);
31223
- if (!existsSync7(join12(skillsDir, skillName)))
31846
+ if (!existsSync8(join13(skillsDir, skillName)))
31224
31847
  return false;
31225
31848
  const meta = loadMeta(skillsDir);
31226
31849
  const disabled = new Set(meta.disabled || []);
@@ -31233,7 +31856,7 @@ function disableSkill(name, targetDir = process.cwd()) {
31233
31856
  return true;
31234
31857
  }
31235
31858
  function enableSkill(name, targetDir = process.cwd()) {
31236
- const skillsDir = join12(targetDir, ".skills");
31859
+ const skillsDir = join13(targetDir, ".skills");
31237
31860
  const meta = loadMeta(skillsDir);
31238
31861
  const disabled = new Set(meta.disabled || []);
31239
31862
  if (!disabled.has(name))
@@ -31245,24 +31868,24 @@ function enableSkill(name, targetDir = process.cwd()) {
31245
31868
  return true;
31246
31869
  }
31247
31870
  function getDisabledSkills(targetDir = process.cwd()) {
31248
- const meta = loadMeta(join12(targetDir, ".skills"));
31871
+ const meta = loadMeta(join13(targetDir, ".skills"));
31249
31872
  return meta.disabled || [];
31250
31873
  }
31251
31874
  function getInstalledSkills(targetDir = process.cwd()) {
31252
- const skillsDir = join12(targetDir, ".skills");
31253
- if (!existsSync7(skillsDir)) {
31875
+ const skillsDir = join13(targetDir, ".skills");
31876
+ if (!existsSync8(skillsDir)) {
31254
31877
  return [];
31255
31878
  }
31256
- return readdirSync5(skillsDir).filter((f) => {
31257
- const fullPath = join12(skillsDir, f);
31879
+ return readdirSync6(skillsDir).filter((f) => {
31880
+ const fullPath = join13(skillsDir, f);
31258
31881
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
31259
31882
  }).map((f) => f.replace("skill-", ""));
31260
31883
  }
31261
31884
  function removeSkill(name, targetDir = process.cwd()) {
31262
31885
  const skillName = normalizeSkillName(name);
31263
- const skillsDir = join12(targetDir, ".skills");
31264
- const skillPath = join12(skillsDir, skillName);
31265
- if (!existsSync7(skillPath)) {
31886
+ const skillsDir = join13(targetDir, ".skills");
31887
+ const skillPath = join13(skillsDir, skillName);
31888
+ if (!existsSync8(skillPath)) {
31266
31889
  return false;
31267
31890
  }
31268
31891
  rmSync2(skillPath, { recursive: true, force: true });
@@ -31273,24 +31896,24 @@ function removeSkill(name, targetDir = process.cwd()) {
31273
31896
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
31274
31897
  const agentDir = `.${agent}`;
31275
31898
  if (scope === "project") {
31276
- return join12(projectDir || process.cwd(), agentDir, "skills");
31899
+ return join13(projectDir || process.cwd(), agentDir, "skills");
31277
31900
  }
31278
- return join12(homedir11(), agentDir, "skills");
31901
+ return join13(homedir12(), agentDir, "skills");
31279
31902
  }
31280
31903
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
31281
31904
  const skillName = normalizeSkillName(name);
31282
- return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
31905
+ return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
31283
31906
  }
31284
31907
  function installSkillForAgent(name, options, generateSkillMd) {
31285
31908
  const { agent, scope = "global", projectDir } = options;
31286
31909
  const skillName = normalizeSkillName(name);
31287
31910
  const sourcePath = getSkillPath(name);
31288
- if (!existsSync7(sourcePath)) {
31911
+ if (!existsSync8(sourcePath)) {
31289
31912
  return { skill: name, success: false, error: `Skill '${name}' not found` };
31290
31913
  }
31291
31914
  let skillMdContent = null;
31292
- const skillMdPath = join12(sourcePath, "SKILL.md");
31293
- if (existsSync7(skillMdPath)) {
31915
+ const skillMdPath = join13(sourcePath, "SKILL.md");
31916
+ if (existsSync8(skillMdPath)) {
31294
31917
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
31295
31918
  } else if (generateSkillMd) {
31296
31919
  skillMdContent = generateSkillMd(name);
@@ -31300,8 +31923,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31300
31923
  }
31301
31924
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31302
31925
  if (scope === "global") {
31303
- const agentBaseDir2 = join12(homedir11(), `.${agent}`);
31304
- if (!existsSync7(agentBaseDir2)) {
31926
+ const agentBaseDir2 = join13(homedir12(), `.${agent}`);
31927
+ if (!existsSync8(agentBaseDir2)) {
31305
31928
  const agentLabels = {
31306
31929
  claude: "Claude Code",
31307
31930
  codex: "Codex CLI",
@@ -31324,8 +31947,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
31324
31947
  }
31325
31948
  }
31326
31949
  try {
31327
- mkdirSync11(destDir, { recursive: true });
31328
- writeFileSync6(join12(destDir, "SKILL.md"), skillMdContent);
31950
+ mkdirSync12(destDir, { recursive: true });
31951
+ writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
31329
31952
  return { skill: name, success: true, path: destDir };
31330
31953
  } catch (error) {
31331
31954
  return {
@@ -31338,7 +31961,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
31338
31961
  function removeSkillForAgent(name, options) {
31339
31962
  const { agent, scope = "global", projectDir } = options;
31340
31963
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
31341
- if (!existsSync7(destDir)) {
31964
+ if (!existsSync8(destDir)) {
31342
31965
  return false;
31343
31966
  }
31344
31967
  rmSync2(destDir, { recursive: true, force: true });
@@ -33262,7 +33885,7 @@ __export(exports_cron_manager, {
33262
33885
  deleteCronJob: () => deleteCronJob,
33263
33886
  createCronJob: () => createCronJob
33264
33887
  });
33265
- import { randomUUID as randomUUID11 } from "crypto";
33888
+ import { randomUUID as randomUUID12 } from "crypto";
33266
33889
  function ensureCronTable() {
33267
33890
  const db2 = getDatabase();
33268
33891
  db2.exec(`
@@ -33291,7 +33914,7 @@ function ensureCronTable() {
33291
33914
  function createCronJob(schedule, task, name) {
33292
33915
  ensureCronTable();
33293
33916
  const db2 = getDatabase();
33294
- const id = randomUUID11();
33917
+ const id = randomUUID12();
33295
33918
  db2.prepare(`
33296
33919
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
33297
33920
  VALUES (?, ?, ?, ?, 1)
@@ -33349,7 +33972,7 @@ function getCronEvents(jobId, limit = 10) {
33349
33972
  async function executeCronJob(job) {
33350
33973
  ensureCronTable();
33351
33974
  const db2 = getDatabase();
33352
- const eventId = randomUUID11();
33975
+ const eventId = randomUUID12();
33353
33976
  const startedAt = new Date().toISOString();
33354
33977
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
33355
33978
  try {
@@ -33440,7 +34063,7 @@ __export(exports_url_watcher, {
33440
34063
  deleteWatchJob: () => deleteWatchJob,
33441
34064
  createWatchJob: () => createWatchJob
33442
34065
  });
33443
- import { randomUUID as randomUUID12 } from "crypto";
34066
+ import { randomUUID as randomUUID13 } from "crypto";
33444
34067
  import { createHash } from "crypto";
33445
34068
  function ensureWatchTables() {
33446
34069
  const db2 = getDatabase();
@@ -33472,7 +34095,7 @@ function ensureWatchTables() {
33472
34095
  function createWatchJob(url, schedule, opts) {
33473
34096
  ensureWatchTables();
33474
34097
  const db2 = getDatabase();
33475
- const id = randomUUID12();
34098
+ const id = randomUUID13();
33476
34099
  db2.prepare(`
33477
34100
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
33478
34101
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -33508,7 +34131,7 @@ function getWatchEvents(watchId, limit = 20) {
33508
34131
  async function checkWatchJob(job) {
33509
34132
  ensureWatchTables();
33510
34133
  const db2 = getDatabase();
33511
- const eventId = randomUUID12();
34134
+ const eventId = randomUUID13();
33512
34135
  const checkedAt = new Date().toISOString();
33513
34136
  let newContent = "";
33514
34137
  try {
@@ -33684,7 +34307,7 @@ var exports_mcp = {};
33684
34307
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33685
34308
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33686
34309
  import { readFileSync as readFileSync8 } from "fs";
33687
- import { join as join13 } from "path";
34310
+ import { join as join14 } from "path";
33688
34311
  function json(data) {
33689
34312
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33690
34313
  }
@@ -33749,7 +34372,7 @@ var init_mcp = __esm(async () => {
33749
34372
  init_dialogs();
33750
34373
  init_profiles();
33751
34374
  init_types();
33752
- _pkg = JSON.parse(readFileSync8(join13(import.meta.dir, "../../package.json"), "utf8"));
34375
+ _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
33753
34376
  networkLogCleanup = new Map;
33754
34377
  consoleCaptureCleanup = new Map;
33755
34378
  harCaptures = new Map;
@@ -33757,7 +34380,7 @@ var init_mcp = __esm(async () => {
33757
34380
  name: "@hasna/browser",
33758
34381
  version: "0.0.1"
33759
34382
  });
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.", {
34383
+ 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
34384
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
33762
34385
  use_case: exports_external.string().optional(),
33763
34386
  project_id: exports_external.string().optional(),
@@ -33768,9 +34391,11 @@ var init_mcp = __esm(async () => {
33768
34391
  viewport_height: exports_external.number().optional().default(720),
33769
34392
  stealth: exports_external.boolean().optional().default(false),
33770
34393
  auto_gallery: exports_external.boolean().optional().default(false),
34394
+ storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
33771
34395
  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 }) => {
34396
+ tags: exports_external.array(exports_external.string()).optional(),
34397
+ 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")
34398
+ }, 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
34399
  try {
33775
34400
  if (agent_id && !force_new) {
33776
34401
  const existing = getActiveSessionForAgent2(agent_id);
@@ -33786,7 +34411,9 @@ var init_mcp = __esm(async () => {
33786
34411
  headless,
33787
34412
  viewport: { width: viewport_width, height: viewport_height },
33788
34413
  stealth,
33789
- autoGallery: auto_gallery
34414
+ autoGallery: auto_gallery,
34415
+ storageState: storage_state,
34416
+ cdpUrl: cdp_url
33790
34417
  });
33791
34418
  if (tags?.length) {
33792
34419
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -33946,7 +34573,7 @@ var init_mcp = __esm(async () => {
33946
34573
  return err(e);
33947
34574
  }
33948
34575
  });
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 }) => {
34576
+ 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
34577
  try {
33951
34578
  const sid = resolveSessionId(session_id);
33952
34579
  const page = getSessionPage(sid);
@@ -33957,14 +34584,17 @@ var init_mcp = __esm(async () => {
33957
34584
  }
33958
34585
  if (!selector)
33959
34586
  return err(new Error("Either ref or selector is required"));
33960
- await click(page, selector, { button, timeout });
33961
- logEvent(sid, "click", { selector, method: "selector" });
34587
+ const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
34588
+ logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
34589
+ if (healInfo.healed) {
34590
+ return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
34591
+ }
33962
34592
  return json({ clicked: selector, method: "selector" });
33963
34593
  } catch (e) {
33964
34594
  return errWithScreenshot(e, session_id);
33965
34595
  }
33966
34596
  });
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 }) => {
34597
+ 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
34598
  try {
33969
34599
  const sid = resolveSessionId(session_id);
33970
34600
  const page = getSessionPage(sid);
@@ -33975,8 +34605,11 @@ var init_mcp = __esm(async () => {
33975
34605
  }
33976
34606
  if (!selector)
33977
34607
  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) });
34608
+ const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
34609
+ logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
34610
+ if (healInfo.healed) {
34611
+ return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
34612
+ }
33980
34613
  return json({ typed: text, selector, method: "selector" });
33981
34614
  } catch (e) {
33982
34615
  return errWithScreenshot(e, session_id);
@@ -34070,20 +34703,32 @@ var init_mcp = __esm(async () => {
34070
34703
  return err(e);
34071
34704
  }
34072
34705
  });
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 }) => {
34706
+ 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
34707
  try {
34075
34708
  const sid = resolveSessionId(session_id);
34076
34709
  const page = getSessionPage(sid);
34077
- return json({ text: await getText(page, selector) });
34710
+ const text = await getText(page, selector);
34711
+ if (sanitize) {
34712
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
34713
+ const sanitized = sanitizeText2(text);
34714
+ return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
34715
+ }
34716
+ return json({ text });
34078
34717
  } catch (e) {
34079
34718
  return err(e);
34080
34719
  }
34081
34720
  });
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 }) => {
34721
+ 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
34722
  try {
34084
34723
  const sid = resolveSessionId(session_id);
34085
34724
  const page = getSessionPage(sid);
34086
- return json({ html: await getHTML(page, selector) });
34725
+ const html = await getHTML(page, selector);
34726
+ if (sanitize) {
34727
+ const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
34728
+ const sanitized = sanitizeHTML2(html);
34729
+ return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
34730
+ }
34731
+ return json({ html });
34087
34732
  } catch (e) {
34088
34733
  return err(e);
34089
34734
  }
@@ -34098,16 +34743,32 @@ var init_mcp = __esm(async () => {
34098
34743
  return err(e);
34099
34744
  }
34100
34745
  });
34101
- server.tool("browser_extract", "Extract content from the page in a specified format", {
34746
+ server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
34102
34747
  session_id: exports_external.string().optional(),
34103
34748
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
34104
34749
  selector: exports_external.string().optional(),
34105
- schema: exports_external.record(exports_external.string()).optional()
34106
- }, async ({ session_id, format, selector, schema }) => {
34750
+ schema: exports_external.record(exports_external.string()).optional(),
34751
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
34752
+ }, async ({ session_id, format, selector, schema, sanitize }) => {
34107
34753
  try {
34108
34754
  const sid = resolveSessionId(session_id);
34109
34755
  const page = getSessionPage(sid);
34110
34756
  const result = await extract(page, { format, selector, schema });
34757
+ if (sanitize) {
34758
+ const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
34759
+ if (result.text) {
34760
+ const s = sanitizeText2(result.text);
34761
+ result.text = s.text;
34762
+ result.stripped = s.stripped;
34763
+ result.warnings = s.warnings;
34764
+ }
34765
+ if (result.html) {
34766
+ const s = sanitizeHTML2(result.html);
34767
+ result.html = s.text;
34768
+ result.stripped = s.stripped;
34769
+ result.warnings = s.warnings;
34770
+ }
34771
+ }
34111
34772
  return json(result);
34112
34773
  } catch (e) {
34113
34774
  return err(e);
@@ -34124,17 +34785,27 @@ var init_mcp = __esm(async () => {
34124
34785
  return err(e);
34125
34786
  }
34126
34787
  });
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.", {
34788
+ 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
34789
  session_id: exports_external.string().optional(),
34129
34790
  compact: exports_external.boolean().optional().default(true),
34130
34791
  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 }) => {
34792
+ full_tree: exports_external.boolean().optional().default(false),
34793
+ sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
34794
+ }, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
34133
34795
  try {
34134
34796
  const sid = resolveSessionId(session_id);
34135
34797
  const page = getSessionPage(sid);
34136
34798
  const result = await takeSnapshot(page, sid);
34137
34799
  setLastSnapshot(sid, result);
34800
+ let injection_warnings;
34801
+ if (sanitize) {
34802
+ const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
34803
+ const sanitized = sanitizeText2(result.tree);
34804
+ if (sanitized.stripped > 0) {
34805
+ injection_warnings = sanitized.warnings;
34806
+ result.tree = sanitized.text;
34807
+ }
34808
+ }
34138
34809
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
34139
34810
  const limitedRefs = Object.fromEntries(refEntries);
34140
34811
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -34146,27 +34817,29 @@ var init_mcp = __esm(async () => {
34146
34817
  interactive_count: result.interactive_count,
34147
34818
  shown_count: refEntries.length,
34148
34819
  truncated,
34149
- refs: limitedRefs
34820
+ refs: limitedRefs,
34821
+ ...injection_warnings ? { injection_warnings } : {}
34150
34822
  });
34151
34823
  }
34152
34824
  const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
34153
34825
  ... (truncated \u2014 use full_tree=true for complete)` : "");
34154
- return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
34826
+ return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
34155
34827
  } catch (e) {
34156
34828
  return err(e);
34157
34829
  }
34158
34830
  });
34159
- server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
34831
+ 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
34832
  session_id: exports_external.string().optional(),
34161
- selector: exports_external.string().optional(),
34833
+ selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
34162
34834
  full_page: exports_external.boolean().optional().default(false),
34163
34835
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
34164
34836
  quality: exports_external.number().optional().default(60),
34165
34837
  max_width: exports_external.number().optional().default(800),
34166
34838
  compress: exports_external.boolean().optional().default(true),
34167
34839
  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 }) => {
34840
+ annotate: exports_external.boolean().optional().default(false),
34841
+ 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).")
34842
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
34170
34843
  try {
34171
34844
  const sid = resolveSessionId(session_id);
34172
34845
  const page = getSessionPage(sid);
@@ -34183,7 +34856,9 @@ var init_mcp = __esm(async () => {
34183
34856
  annotation_count: annotated.annotations.length
34184
34857
  });
34185
34858
  }
34186
- const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
34859
+ const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
34860
+ const effectiveQuality = detail === "high" ? 75 : quality;
34861
+ const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
34187
34862
  result.url = page.url();
34188
34863
  try {
34189
34864
  const buf = Buffer.from(result.base64, "base64");
@@ -34192,12 +34867,12 @@ var init_mcp = __esm(async () => {
34192
34867
  result.download_id = dl.id;
34193
34868
  } catch {}
34194
34869
  result.estimated_tokens = Math.ceil(result.base64.length / 4);
34195
- if (result.base64.length > 20000) {
34870
+ if (detail !== "high" && result.base64.length > 40000) {
34196
34871
  result.base64_truncated = true;
34197
34872
  result.full_image_path = result.path;
34198
34873
  result.base64 = result.thumbnail_base64 ?? "";
34199
34874
  }
34200
- logEvent(sid, "screenshot", { path: result.path });
34875
+ logEvent(sid, "screenshot", { path: result.path, detail, selector });
34201
34876
  return json(result);
34202
34877
  } catch (e) {
34203
34878
  return err(e);
@@ -34603,6 +35278,94 @@ var init_mcp = __esm(async () => {
34603
35278
  return err(e);
34604
35279
  }
34605
35280
  });
35281
+ 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 }) => {
35282
+ try {
35283
+ const sid = resolveSessionId(session_id);
35284
+ const page = getSessionPage(sid);
35285
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35286
+ const path = await saveStateFromPage2(page, name);
35287
+ return json({ saved: true, name, path });
35288
+ } catch (e) {
35289
+ return err(e);
35290
+ }
35291
+ });
35292
+ server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
35293
+ try {
35294
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35295
+ const states = listStates2();
35296
+ return json({ states, count: states.length });
35297
+ } catch (e) {
35298
+ return err(e);
35299
+ }
35300
+ });
35301
+ server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
35302
+ try {
35303
+ const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35304
+ return json({ deleted: deleteState2(name), name });
35305
+ } catch (e) {
35306
+ return err(e);
35307
+ }
35308
+ });
35309
+ 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 }) => {
35310
+ try {
35311
+ const sid = resolveSessionId(session_id);
35312
+ const page = getSessionPage(sid);
35313
+ if (start_url)
35314
+ await navigate(page, start_url);
35315
+ const recording = startRecording(sid, `auth-${name}`, page.url());
35316
+ return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
35317
+ } catch (e) {
35318
+ return err(e);
35319
+ }
35320
+ });
35321
+ 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 }) => {
35322
+ try {
35323
+ const sid = resolveSessionId(session_id);
35324
+ const page = getSessionPage(sid);
35325
+ const recording = stopRecording(recording_id);
35326
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
35327
+ const statePath2 = await saveStateFromPage2(page, name);
35328
+ let domain = "";
35329
+ try {
35330
+ domain = new URL(page.url()).hostname;
35331
+ } catch {}
35332
+ const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
35333
+ const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
35334
+ return json({ flow, recording_steps: recording.steps.length });
35335
+ } catch (e) {
35336
+ return err(e);
35337
+ }
35338
+ });
35339
+ 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 }) => {
35340
+ try {
35341
+ const sid = resolveSessionId(session_id);
35342
+ const page = getSessionPage(sid);
35343
+ const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
35344
+ const flow = getAuthFlowByName2(name);
35345
+ if (!flow)
35346
+ return err(new Error(`Auth flow '${name}' not found`));
35347
+ const result = await tryReplayAuth2(page, flow.domain);
35348
+ return json(result);
35349
+ } catch (e) {
35350
+ return err(e);
35351
+ }
35352
+ });
35353
+ server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
35354
+ try {
35355
+ const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
35356
+ return json({ flows: listAuthFlows2() });
35357
+ } catch (e) {
35358
+ return err(e);
35359
+ }
35360
+ });
35361
+ server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
35362
+ try {
35363
+ const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
35364
+ return json({ deleted: deleteAuthFlow2(name) });
35365
+ } catch (e) {
35366
+ return err(e);
35367
+ }
35368
+ });
34606
35369
  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
35370
  try {
34608
35371
  const sid = resolveSessionId(session_id);
@@ -34613,20 +35376,45 @@ var init_mcp = __esm(async () => {
34613
35376
  return err(e);
34614
35377
  }
34615
35378
  });
34616
- server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
35379
+ 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
35380
  session_id: exports_external.string().optional(),
34618
35381
  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 }) => {
35382
+ submit_selector: exports_external.string().optional(),
35383
+ self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
35384
+ }, async ({ session_id, fields, submit_selector, self_heal }) => {
34621
35385
  try {
34622
35386
  const sid = resolveSessionId(session_id);
34623
35387
  const page = getSessionPage(sid);
34624
- const result = await fillForm(page, fields, submit_selector);
35388
+ const result = await fillForm(page, fields, submit_selector, self_heal);
34625
35389
  return json(result);
34626
35390
  } catch (e) {
34627
35391
  return errWithScreenshot(e, session_id);
34628
35392
  }
34629
35393
  });
35394
+ 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.", {
35395
+ session_id: exports_external.string().optional(),
35396
+ 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')"),
35397
+ click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
35398
+ model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
35399
+ }, async ({ session_id, description, click: doClick, model }) => {
35400
+ try {
35401
+ const sid = resolveSessionId(session_id);
35402
+ const page = getSessionPage(sid);
35403
+ if (doClick) {
35404
+ const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
35405
+ const result = await clickByVision2(page, description, { model });
35406
+ logEvent(sid, "vision_click", { query: description, ...result });
35407
+ return json(result);
35408
+ } else {
35409
+ const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
35410
+ const result = await findElementByVision2(page, description, { model });
35411
+ logEvent(sid, "vision_find", { query: description, ...result });
35412
+ return json(result);
35413
+ }
35414
+ } catch (e) {
35415
+ return err(e);
35416
+ }
35417
+ });
34630
35418
  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
35419
  try {
34632
35420
  const sid = resolveSessionId(session_id);
@@ -35047,6 +35835,7 @@ var init_mcp = __esm(async () => {
35047
35835
  { tool: "browser_wait", description: "Wait for a selector to appear" },
35048
35836
  { tool: "browser_wait_for_text", description: "Wait for text to appear" },
35049
35837
  { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
35838
+ { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
35050
35839
  { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
35051
35840
  ],
35052
35841
  Extraction: [
@@ -35075,7 +35864,10 @@ var init_mcp = __esm(async () => {
35075
35864
  { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
35076
35865
  { tool: "browser_profile_load", description: "Load and apply a saved profile" },
35077
35866
  { tool: "browser_profile_list", description: "List saved profiles" },
35078
- { tool: "browser_profile_delete", description: "Delete a saved profile" }
35867
+ { tool: "browser_profile_delete", description: "Delete a saved profile" },
35868
+ { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
35869
+ { tool: "browser_session_list_states", description: "List saved storage states" },
35870
+ { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
35079
35871
  ],
35080
35872
  Network: [
35081
35873
  { tool: "browser_network_log", description: "Get captured network requests" },
@@ -35099,6 +35891,13 @@ var init_mcp = __esm(async () => {
35099
35891
  { tool: "browser_record_replay", description: "Replay a recorded sequence" },
35100
35892
  { tool: "browser_recordings_list", description: "List all recordings" }
35101
35893
  ],
35894
+ Auth: [
35895
+ { tool: "browser_auth_record", description: "Start recording a login flow" },
35896
+ { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
35897
+ { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
35898
+ { tool: "browser_auth_list", description: "List all saved auth flows" },
35899
+ { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
35900
+ ],
35102
35901
  Crawl: [
35103
35902
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
35104
35903
  ],
@@ -35155,7 +35954,8 @@ var init_mcp = __esm(async () => {
35155
35954
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
35156
35955
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
35157
35956
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
35158
- { tool: "browser_watch_stop", description: "Stop DOM watcher" }
35957
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" },
35958
+ { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
35159
35959
  ]
35160
35960
  };
35161
35961
  const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
@@ -35438,6 +36238,82 @@ var init_mcp = __esm(async () => {
35438
36238
  return err(e);
35439
36239
  }
35440
36240
  });
36241
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
36242
+ actions: exports_external.array(exports_external.object({
36243
+ session_id: exports_external.string().describe("Target session ID"),
36244
+ tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
36245
+ args: exports_external.record(exports_external.unknown()).optional().default({})
36246
+ })),
36247
+ timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
36248
+ }, async ({ actions, timeout }) => {
36249
+ try {
36250
+ const t0 = Date.now();
36251
+ const promises = actions.map(async (action, index) => {
36252
+ try {
36253
+ const sid = action.session_id;
36254
+ const page = getSessionPage(sid);
36255
+ const args = action.args;
36256
+ const toolName = action.tool.replace(/^browser_/, "");
36257
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
36258
+ const actionPromise = (async () => {
36259
+ switch (toolName) {
36260
+ case "navigate": {
36261
+ await navigate(page, args.url);
36262
+ const title = await page.title();
36263
+ return { url: page.url(), title };
36264
+ }
36265
+ case "screenshot": {
36266
+ const result2 = await takeScreenshot(page, {
36267
+ maxWidth: args.max_width ?? 800,
36268
+ quality: args.quality ?? 60
36269
+ });
36270
+ return { path: result2.path, size_bytes: result2.size_bytes };
36271
+ }
36272
+ case "click": {
36273
+ if (args.selector)
36274
+ await click(page, args.selector);
36275
+ return { clicked: args.selector };
36276
+ }
36277
+ case "type": {
36278
+ if (args.selector && args.text)
36279
+ await type(page, args.selector, args.text);
36280
+ return { typed: args.text };
36281
+ }
36282
+ case "get_text": {
36283
+ const text = await getText(page);
36284
+ return { text: text.slice(0, 1000), length: text.length };
36285
+ }
36286
+ case "get_links": {
36287
+ const links = await getLinks(page);
36288
+ return { links, count: links.length };
36289
+ }
36290
+ case "snapshot": {
36291
+ const snap = await takeSnapshot(page, sid);
36292
+ return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
36293
+ }
36294
+ case "evaluate": {
36295
+ const result2 = await page.evaluate(args.expression);
36296
+ return { result: result2 };
36297
+ }
36298
+ default:
36299
+ return { error: `Unknown tool: ${action.tool}` };
36300
+ }
36301
+ })();
36302
+ const result = await Promise.race([actionPromise, timeoutPromise]);
36303
+ return { index, session_id: sid, tool: action.tool, success: true, result };
36304
+ } catch (e) {
36305
+ return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
36306
+ }
36307
+ });
36308
+ const results = await Promise.all(promises);
36309
+ const duration_ms = Date.now() - t0;
36310
+ const succeeded = results.filter((r) => r.success).length;
36311
+ const failed = results.filter((r) => !r.success).length;
36312
+ return json({ results, duration_ms, succeeded, failed, total: actions.length });
36313
+ } catch (e) {
36314
+ return err(e);
36315
+ }
36316
+ });
35441
36317
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
35442
36318
  try {
35443
36319
  return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
@@ -35592,10 +36468,10 @@ __export(exports_snapshots, {
35592
36468
  deleteSnapshot: () => deleteSnapshot,
35593
36469
  createSnapshot: () => createSnapshot
35594
36470
  });
35595
- import { randomUUID as randomUUID13 } from "crypto";
36471
+ import { randomUUID as randomUUID14 } from "crypto";
35596
36472
  function createSnapshot(data) {
35597
36473
  const db2 = getDatabase();
35598
- const id = randomUUID13();
36474
+ const id = randomUUID14();
35599
36475
  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
36476
  return getSnapshot(id);
35601
36477
  }
@@ -35621,8 +36497,8 @@ var init_snapshots = __esm(() => {
35621
36497
 
35622
36498
  // src/server/index.ts
35623
36499
  var exports_server = {};
35624
- import { join as join14 } from "path";
35625
- import { existsSync as existsSync8 } from "fs";
36500
+ import { join as join15 } from "path";
36501
+ import { existsSync as existsSync9 } from "fs";
35626
36502
  function ok(data, status = 200) {
35627
36503
  return new Response(JSON.stringify(data), {
35628
36504
  status,
@@ -35872,14 +36748,14 @@ var init_server = __esm(() => {
35872
36748
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
35873
36749
  const id = path.split("/")[3];
35874
36750
  const entry = getEntry(id);
35875
- if (!entry?.thumbnail_path || !existsSync8(entry.thumbnail_path))
36751
+ if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
35876
36752
  return notFound("Thumbnail not found");
35877
36753
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
35878
36754
  }
35879
36755
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
35880
36756
  const id = path.split("/")[3];
35881
36757
  const entry = getEntry(id);
35882
- if (!entry?.path || !existsSync8(entry.path))
36758
+ if (!entry?.path || !existsSync9(entry.path))
35883
36759
  return notFound("Image not found");
35884
36760
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
35885
36761
  }
@@ -35907,7 +36783,7 @@ var init_server = __esm(() => {
35907
36783
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
35908
36784
  const id = path.split("/")[3];
35909
36785
  const file = getDownload(id);
35910
- if (!file || !existsSync8(file.path))
36786
+ if (!file || !existsSync9(file.path))
35911
36787
  return notFound("Download not found");
35912
36788
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
35913
36789
  }
@@ -35915,13 +36791,13 @@ var init_server = __esm(() => {
35915
36791
  const id = path.split("/")[3];
35916
36792
  return ok({ deleted: deleteDownload(id) });
35917
36793
  }
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)) {
36794
+ const dashboardDist = join15(import.meta.dir, "../../dashboard/dist");
36795
+ if (existsSync9(dashboardDist)) {
36796
+ const filePath = path === "/" ? join15(dashboardDist, "index.html") : join15(dashboardDist, path);
36797
+ if (existsSync9(filePath)) {
35922
36798
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
35923
36799
  }
35924
- return new Response(Bun.file(join14(dashboardDist, "index.html")), { headers: CORS_HEADERS });
36800
+ return new Response(Bun.file(join15(dashboardDist, "index.html")), { headers: CORS_HEADERS });
35925
36801
  }
35926
36802
  if (path === "/" || path === "") {
35927
36803
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -35964,9 +36840,9 @@ init_recorder();
35964
36840
  init_recordings();
35965
36841
  init_lightpanda();
35966
36842
  import { readFileSync as readFileSync9 } from "fs";
35967
- import { join as join15 } from "path";
36843
+ import { join as join16 } from "path";
35968
36844
  import chalk from "chalk";
35969
- var pkg = JSON.parse(readFileSync9(join15(import.meta.dir, "../../package.json"), "utf8"));
36845
+ var pkg = JSON.parse(readFileSync9(join16(import.meta.dir, "../../package.json"), "utf8"));
35970
36846
  var program2 = new Command;
35971
36847
  program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
35972
36848
  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 +36976,22 @@ sessionCmd.command("close <id>").description("Close a session").action(async (id
36100
36976
  await closeSession2(id);
36101
36977
  console.log(chalk.green(`\u2713 Session closed: ${id}`));
36102
36978
  });
36979
+ sessionCmd.command("save-state <name>").description("Save current session auth state for reuse").requiredOption("--session <id>", "Session ID").action(async (name, opts) => {
36980
+ const page = getSessionPage(opts.session);
36981
+ const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36982
+ const path = await saveStateFromPage2(page, name);
36983
+ console.log(chalk.green(`\u2713 State saved: ${name}`));
36984
+ console.log(chalk.gray(` Path: ${path}`));
36985
+ });
36986
+ sessionCmd.command("list-states").description("List saved auth states").action(async () => {
36987
+ const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
36988
+ const states = listStates2();
36989
+ if (states.length === 0) {
36990
+ console.log(chalk.gray("No saved states"));
36991
+ return;
36992
+ }
36993
+ states.forEach((s) => console.log(`${s.name} ${chalk.gray(s.modified)}`));
36994
+ });
36103
36995
  var recordCmd = program2.command("record").description("Manage action recordings");
36104
36996
  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
36997
  const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
@@ -36162,6 +37054,19 @@ projectCmd.command("list").description("List all projects").action(() => {
36162
37054
  projects.forEach((p) => console.log(`${p.id} "${p.name}" ${p.path}`));
36163
37055
  }
36164
37056
  });
37057
+ 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) => {
37058
+ const cdpUrl = `http://${opts.host}:${opts.port}`;
37059
+ const { session, page } = await createSession2({ cdpUrl });
37060
+ const title = await page.title();
37061
+ const url = page.url();
37062
+ if (opts.json) {
37063
+ console.log(JSON.stringify({ session_id: session.id, url, title, cdp_url: cdpUrl }));
37064
+ } else {
37065
+ console.log(chalk.green(`\u2713 Attached to Chrome at ${cdpUrl}`));
37066
+ console.log(chalk.blue(` Session: ${session.id}`));
37067
+ console.log(chalk.blue(` Page: ${title} (${url})`));
37068
+ }
37069
+ });
36165
37070
  program2.command("install-browser").description("Install a browser engine").option("--engine <engine>", "Engine to install: lightpanda|chromium", "chromium").action(async (opts) => {
36166
37071
  if (opts.engine === "chromium") {
36167
37072
  const { execSync: execSync3 } = await import("child_process");
@@ -36253,11 +37158,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
36253
37158
  });
36254
37159
  galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
36255
37160
  const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
36256
- const { existsSync: existsSync9 } = await import("fs");
37161
+ const { existsSync: existsSync10 } = await import("fs");
36257
37162
  const entries = listEntries2({ limit: 9999 });
36258
37163
  let removed = 0;
36259
37164
  for (const e of entries) {
36260
- if (!existsSync9(e.path)) {
37165
+ if (!existsSync10(e.path)) {
36261
37166
  deleteEntry2(e.id);
36262
37167
  removed++;
36263
37168
  }