@hasna/browser 0.0.7 → 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
@@ -2309,6 +2309,55 @@ function runMigrations(db) {
2309
2309
  CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
2310
2310
  CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
2311
2311
  `
2312
+ },
2313
+ {
2314
+ version: 3,
2315
+ sql: `
2316
+ -- Session lock/claim for multi-agent ownership
2317
+ ALTER TABLE sessions ADD COLUMN locked_by TEXT;
2318
+ ALTER TABLE sessions ADD COLUMN locked_at TEXT;
2319
+ `
2320
+ },
2321
+ {
2322
+ version: 4,
2323
+ sql: `
2324
+ CREATE TABLE IF NOT EXISTS session_events (
2325
+ id TEXT PRIMARY KEY,
2326
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
2327
+ event_type TEXT NOT NULL,
2328
+ details TEXT DEFAULT '{}',
2329
+ timestamp TEXT DEFAULT (datetime('now'))
2330
+ );
2331
+ CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp);
2332
+ `
2333
+ },
2334
+ {
2335
+ version: 5,
2336
+ sql: `
2337
+ CREATE TABLE IF NOT EXISTS session_tags (
2338
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
2339
+ tag TEXT NOT NULL,
2340
+ PRIMARY KEY (session_id, tag)
2341
+ );
2342
+ CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
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
+ `
2312
2361
  }
2313
2362
  ];
2314
2363
  for (const m of migrations) {
@@ -2324,6 +2373,28 @@ var _db = null, _dbPath = null;
2324
2373
  var init_schema = () => {};
2325
2374
 
2326
2375
  // src/db/sessions.ts
2376
+ var exports_sessions = {};
2377
+ __export(exports_sessions, {
2378
+ updateSessionStatus: () => updateSessionStatus,
2379
+ unlockSession: () => unlockSession,
2380
+ transferSession: () => transferSession,
2381
+ renameSession: () => renameSession,
2382
+ removeSessionTag: () => removeSessionTag,
2383
+ lockSession: () => lockSession,
2384
+ listSessionsByTag: () => listSessionsByTag,
2385
+ listSessions: () => listSessions,
2386
+ isSessionLocked: () => isSessionLocked,
2387
+ getSessionTags: () => getSessionTags,
2388
+ getSessionByName: () => getSessionByName,
2389
+ getSession: () => getSession,
2390
+ getDefaultActiveSession: () => getDefaultActiveSession,
2391
+ getActiveSessionForAgent: () => getActiveSessionForAgent,
2392
+ deleteSession: () => deleteSession,
2393
+ createSession: () => createSession,
2394
+ countActiveSessions: () => countActiveSessions,
2395
+ closeSession: () => closeSession,
2396
+ addSessionTag: () => addSessionTag
2397
+ });
2327
2398
  import { randomUUID } from "crypto";
2328
2399
  function createSession(data) {
2329
2400
  const db = getDatabase();
@@ -2376,8 +2447,81 @@ function updateSessionStatus(id, status) {
2376
2447
  return getSession(id);
2377
2448
  }
2378
2449
  function closeSession(id) {
2450
+ const db = getDatabase();
2451
+ db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
2379
2452
  return updateSessionStatus(id, "closed");
2380
2453
  }
2454
+ function lockSession(id, agentId) {
2455
+ const db = getDatabase();
2456
+ const session = getSession(id);
2457
+ if (session.status !== "active")
2458
+ throw new SessionNotFoundError(id);
2459
+ const row = db.query("SELECT locked_by FROM sessions WHERE id = ?").get(id);
2460
+ if (row?.locked_by && row.locked_by !== agentId) {
2461
+ throw new Error(`Session locked by agent ${row.locked_by}`);
2462
+ }
2463
+ db.prepare("UPDATE sessions SET locked_by = ?, locked_at = datetime('now') WHERE id = ?").run(agentId, id);
2464
+ return getSession(id);
2465
+ }
2466
+ function unlockSession(id, agentId) {
2467
+ const db = getDatabase();
2468
+ if (agentId) {
2469
+ const row = db.query("SELECT locked_by FROM sessions WHERE id = ?").get(id);
2470
+ if (row?.locked_by && row.locked_by !== agentId) {
2471
+ throw new Error(`Session locked by agent ${row.locked_by}, not ${agentId}`);
2472
+ }
2473
+ }
2474
+ db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
2475
+ return getSession(id);
2476
+ }
2477
+ function isSessionLocked(id) {
2478
+ const db = getDatabase();
2479
+ const row = db.query("SELECT locked_by, locked_at FROM sessions WHERE id = ?").get(id);
2480
+ if (!row)
2481
+ throw new SessionNotFoundError(id);
2482
+ return { locked: !!row.locked_by, locked_by: row.locked_by ?? undefined, locked_at: row.locked_at ?? undefined };
2483
+ }
2484
+ function transferSession(id, toAgentId) {
2485
+ const db = getDatabase();
2486
+ db.prepare("UPDATE sessions SET agent_id = ?, locked_by = ?, locked_at = datetime('now') WHERE id = ?").run(toAgentId, toAgentId, id);
2487
+ return getSession(id);
2488
+ }
2489
+ function getActiveSessionForAgent(agentId) {
2490
+ const db = getDatabase();
2491
+ return db.query("SELECT * FROM sessions WHERE agent_id = ? AND status = 'active' ORDER BY created_at DESC LIMIT 1").get(agentId) ?? null;
2492
+ }
2493
+ function getDefaultActiveSession() {
2494
+ const db = getDatabase();
2495
+ const rows = db.query("SELECT * FROM sessions WHERE status = 'active' ORDER BY created_at DESC LIMIT 2").all();
2496
+ return rows.length === 1 ? rows[0] : null;
2497
+ }
2498
+ function countActiveSessions() {
2499
+ const db = getDatabase();
2500
+ const row = db.query("SELECT COUNT(*) as count FROM sessions WHERE status = 'active'").get();
2501
+ return row?.count ?? 0;
2502
+ }
2503
+ function deleteSession(id) {
2504
+ const db = getDatabase();
2505
+ db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
2506
+ }
2507
+ function addSessionTag(id, tag) {
2508
+ const db = getDatabase();
2509
+ db.prepare("INSERT OR IGNORE INTO session_tags (session_id, tag) VALUES (?, ?)").run(id, tag);
2510
+ return getSessionTags(id);
2511
+ }
2512
+ function removeSessionTag(id, tag) {
2513
+ const db = getDatabase();
2514
+ db.prepare("DELETE FROM session_tags WHERE session_id = ? AND tag = ?").run(id, tag);
2515
+ return getSessionTags(id);
2516
+ }
2517
+ function getSessionTags(id) {
2518
+ const db = getDatabase();
2519
+ return db.query("SELECT tag FROM session_tags WHERE session_id = ? ORDER BY tag").all(id).map((r) => r.tag);
2520
+ }
2521
+ function listSessionsByTag(tag) {
2522
+ const db = getDatabase();
2523
+ return db.query("SELECT s.* FROM sessions s JOIN session_tags t ON s.id = t.session_id WHERE t.tag = ? ORDER BY s.created_at DESC").all(tag);
2524
+ }
2381
2525
  var init_sessions = __esm(() => {
2382
2526
  init_schema();
2383
2527
  init_types();
@@ -2403,10 +2547,52 @@ async function getPage(browser, options) {
2403
2547
  });
2404
2548
  return context.newPage();
2405
2549
  }
2406
- async function closeBrowser(browser) {
2407
- try {
2408
- await browser.close();
2409
- } catch {}
2550
+
2551
+ class BrowserPool {
2552
+ pool = [];
2553
+ maxSize;
2554
+ options;
2555
+ constructor(maxSize = 3, options) {
2556
+ this.maxSize = maxSize;
2557
+ this.options = options;
2558
+ }
2559
+ async acquire(headless = true) {
2560
+ const available = this.pool.find((e) => !e.inUse);
2561
+ if (available) {
2562
+ available.inUse = true;
2563
+ return available.browser;
2564
+ }
2565
+ if (this.pool.length < this.maxSize) {
2566
+ const browser = await launchPlaywright({ ...this.options, headless });
2567
+ this.pool.push({ browser, inUse: true, createdAt: Date.now() });
2568
+ return browser;
2569
+ }
2570
+ return new Promise((resolve) => {
2571
+ const interval = setInterval(() => {
2572
+ const free = this.pool.find((e) => !e.inUse);
2573
+ if (free) {
2574
+ clearInterval(interval);
2575
+ free.inUse = true;
2576
+ resolve(free.browser);
2577
+ }
2578
+ }, 100);
2579
+ });
2580
+ }
2581
+ release(browser) {
2582
+ const entry = this.pool.find((e) => e.browser === browser);
2583
+ if (entry)
2584
+ entry.inUse = false;
2585
+ }
2586
+ async destroyAll() {
2587
+ await Promise.all(this.pool.map((e) => e.browser.close().catch(() => {})));
2588
+ this.pool = [];
2589
+ }
2590
+ get size() {
2591
+ return this.pool.length;
2592
+ }
2593
+ get available() {
2594
+ return this.pool.filter((e) => !e.inUse).length;
2595
+ }
2410
2596
  }
2411
2597
  var DEFAULT_VIEWPORT;
2412
2598
  var init_playwright = __esm(() => {
@@ -3287,6 +3473,188 @@ var init_dialogs = __esm(() => {
3287
3473
  pendingDialogs = new Map;
3288
3474
  });
3289
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
+
3290
3658
  // src/lib/session.ts
3291
3659
  var exports_session = {};
3292
3660
  __export(exports_session, {
@@ -3294,6 +3662,7 @@ __export(exports_session, {
3294
3662
  renameSession: () => renameSession2,
3295
3663
  listSessions: () => listSessions2,
3296
3664
  isBunSession: () => isBunSession,
3665
+ isAutoGallery: () => isAutoGallery,
3297
3666
  hasActiveHandle: () => hasActiveHandle,
3298
3667
  getTokenBudget: () => getTokenBudget,
3299
3668
  getSessionPage: () => getSessionPage,
@@ -3302,15 +3671,50 @@ __export(exports_session, {
3302
3671
  getSessionBunView: () => getSessionBunView,
3303
3672
  getSessionBrowser: () => getSessionBrowser,
3304
3673
  getSession: () => getSession2,
3674
+ getDefaultSession: () => getDefaultSession,
3305
3675
  getActiveSessions: () => getActiveSessions,
3676
+ getActiveSessionForAgent: () => getActiveSessionForAgent2,
3306
3677
  createSession: () => createSession2,
3678
+ countActiveSessions: () => countActiveSessions2,
3307
3679
  closeSession: () => closeSession2,
3308
- closeAllSessions: () => closeAllSessions
3680
+ closeAllSessions: () => closeAllSessions,
3681
+ browserPool: () => pool
3309
3682
  });
3310
3683
  function createBunProxy(view) {
3311
3684
  return view;
3312
3685
  }
3313
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
+ }
3314
3718
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
3315
3719
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
3316
3720
  let browser = null;
@@ -3335,12 +3739,23 @@ async function createSession2(opts = {}) {
3335
3739
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
3336
3740
  page = await context.newPage();
3337
3741
  } else {
3338
- browser = await launchPlaywright({
3339
- headless: opts.headless ?? true,
3340
- viewport: opts.viewport,
3341
- userAgent: opts.userAgent
3342
- });
3343
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3742
+ browser = await pool.acquire(opts.headless ?? true);
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
+ }
3344
3759
  }
3345
3760
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
3346
3761
  try {
@@ -3393,7 +3808,7 @@ async function createSession2(opts = {}) {
3393
3808
  } catch {}
3394
3809
  }
3395
3810
  }
3396
- handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
3811
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
3397
3812
  if (opts.startUrl) {
3398
3813
  try {
3399
3814
  if (bunView) {
@@ -3419,6 +3834,7 @@ function getSessionPage(sessionId) {
3419
3834
  handles.delete(sessionId);
3420
3835
  throw new SessionNotFoundError(sessionId);
3421
3836
  }
3837
+ handle.lastActivity = Date.now();
3422
3838
  return handle.page;
3423
3839
  }
3424
3840
  function getSessionBunView(sessionId) {
@@ -3466,10 +3882,8 @@ async function closeSession2(sessionId) {
3466
3882
  try {
3467
3883
  await handle.page.context().close();
3468
3884
  } catch {}
3469
- try {
3470
- if (handle.browser)
3471
- await closeBrowser(handle.browser);
3472
- } catch {}
3885
+ if (handle.browser)
3886
+ pool.release(handle.browser);
3473
3887
  }
3474
3888
  handles.delete(sessionId);
3475
3889
  }
@@ -3488,6 +3902,7 @@ async function closeAllSessions() {
3488
3902
  for (const [id] of handles) {
3489
3903
  await closeSession2(id).catch(() => {});
3490
3904
  }
3905
+ await pool.destroyAll();
3491
3906
  }
3492
3907
  function getSessionByName2(name) {
3493
3908
  return getSessionByName(name);
@@ -3499,7 +3914,49 @@ function getTokenBudget(sessionId) {
3499
3914
  const handle = handles.get(sessionId);
3500
3915
  return handle ? handle.tokenBudget : null;
3501
3916
  }
3502
- var handles;
3917
+ function getActiveSessionForAgent2(agentId) {
3918
+ const session = getActiveSessionForAgent(agentId);
3919
+ if (!session)
3920
+ return null;
3921
+ const handle = handles.get(session.id);
3922
+ if (!handle)
3923
+ return null;
3924
+ try {
3925
+ if (handle.bunView)
3926
+ handle.bunView.url();
3927
+ else
3928
+ handle.page.url();
3929
+ } catch {
3930
+ handles.delete(session.id);
3931
+ return null;
3932
+ }
3933
+ return { session, page: handle.page };
3934
+ }
3935
+ function getDefaultSession() {
3936
+ const session = getDefaultActiveSession();
3937
+ if (!session)
3938
+ return null;
3939
+ const handle = handles.get(session.id);
3940
+ if (!handle)
3941
+ return null;
3942
+ try {
3943
+ if (handle.bunView)
3944
+ handle.bunView.url();
3945
+ else
3946
+ handle.page.url();
3947
+ } catch {
3948
+ handles.delete(session.id);
3949
+ return null;
3950
+ }
3951
+ return { session, page: handle.page };
3952
+ }
3953
+ function isAutoGallery(sessionId) {
3954
+ return handles.get(sessionId)?.autoGallery ?? false;
3955
+ }
3956
+ function countActiveSessions2() {
3957
+ return countActiveSessions();
3958
+ }
3959
+ var handles, pool, SESSION_TTL_MS, ttlInterval;
3503
3960
  var init_session = __esm(() => {
3504
3961
  init_types();
3505
3962
  init_types();
@@ -3513,6 +3970,20 @@ var init_session = __esm(() => {
3513
3970
  init_stealth();
3514
3971
  init_dialogs();
3515
3972
  handles = new Map;
3973
+ pool = new BrowserPool(5);
3974
+ SESSION_TTL_MS = parseInt(process.env["SESSION_TTL_MINUTES"] ?? "10", 10) * 60000;
3975
+ ttlInterval = setInterval(async () => {
3976
+ const now = Date.now();
3977
+ for (const [id, handle] of handles) {
3978
+ if (now - handle.lastActivity > SESSION_TTL_MS) {
3979
+ try {
3980
+ await closeSession2(id);
3981
+ } catch {}
3982
+ }
3983
+ }
3984
+ }, 60000);
3985
+ if (ttlInterval.unref)
3986
+ ttlInterval.unref();
3516
3987
  });
3517
3988
 
3518
3989
  // src/lib/snapshot.ts
@@ -3789,6 +4260,66 @@ var init_snapshot = __esm(() => {
3789
4260
  ];
3790
4261
  });
3791
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
+
3792
4323
  // src/lib/actions.ts
3793
4324
  var exports_actions = {};
3794
4325
  __export(exports_actions, {
@@ -3830,11 +4361,22 @@ async function click(page, selector, opts) {
3830
4361
  delay: opts?.delay,
3831
4362
  timeout: opts?.timeout ?? 1e4
3832
4363
  });
3833
- } catch (err) {
3834
- 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")) {
3835
4377
  throw new ElementNotFoundError(selector);
3836
4378
  }
3837
- 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");
3838
4380
  }
3839
4381
  }
3840
4382
  async function type(page, selector, text, opts) {
@@ -3843,17 +4385,35 @@ async function type(page, selector, text, opts) {
3843
4385
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
3844
4386
  }
3845
4387
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
3846
- } catch (err) {
3847
- 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")) {
3848
4400
  throw new ElementNotFoundError(selector);
3849
4401
  }
3850
- 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");
3851
4403
  }
3852
4404
  }
3853
- async function fill(page, selector, value, timeout = 1e4) {
4405
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
3854
4406
  try {
3855
4407
  await page.fill(selector, value, { timeout });
3856
- } 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
+ }
3857
4417
  throw new ElementNotFoundError(selector);
3858
4418
  }
3859
4419
  }
@@ -3978,12 +4538,39 @@ async function clickText(page, text, opts) {
3978
4538
  }
3979
4539
  }, { retries: opts?.retries ?? 1 });
3980
4540
  }
3981
- async function fillForm(page, fields, submitSelector) {
4541
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
3982
4542
  let filled = 0;
3983
4543
  const errors = [];
4544
+ const healedFields = [];
3984
4545
  for (const [selector, value] of Object.entries(fields)) {
3985
4546
  try {
3986
- 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
+ }
3987
4574
  if (!el) {
3988
4575
  errors.push(`${selector}: element not found`);
3989
4576
  continue;
@@ -4010,11 +4597,21 @@ async function fillForm(page, fields, submitSelector) {
4010
4597
  if (submitSelector) {
4011
4598
  try {
4012
4599
  await page.click(submitSelector);
4013
- } catch (err) {
4014
- 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
+ }
4015
4612
  }
4016
4613
  }
4017
- 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 } : {} };
4018
4615
  }
4019
4616
  async function waitForText(page, text, opts) {
4020
4617
  const timeout = opts?.timeout ?? 1e4;
@@ -10870,17 +11467,17 @@ var init_gallery = __esm(() => {
10870
11467
  });
10871
11468
 
10872
11469
  // src/lib/screenshot.ts
10873
- import { join as join3 } from "path";
10874
- import { mkdirSync as mkdirSync3 } from "fs";
10875
- 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";
10876
11473
  function getDataDir2() {
10877
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
11474
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
10878
11475
  }
10879
11476
  function getScreenshotDir(projectId) {
10880
- const base = join3(getDataDir2(), "screenshots");
11477
+ const base = join4(getDataDir2(), "screenshots");
10881
11478
  const date = new Date().toISOString().split("T")[0];
10882
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
10883
- mkdirSync3(dir, { recursive: true });
11479
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
11480
+ mkdirSync4(dir, { recursive: true });
10884
11481
  return dir;
10885
11482
  }
10886
11483
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -10895,7 +11492,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
10895
11492
  }
10896
11493
  }
10897
11494
  async function generateThumbnail(raw, dir, stem) {
10898
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
11495
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
10899
11496
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
10900
11497
  await Bun.write(thumbPath, thumbBuffer);
10901
11498
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -10952,7 +11549,7 @@ async function takeScreenshot(page, opts) {
10952
11549
  const compressedSizeBytes = finalBuffer.length;
10953
11550
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
10954
11551
  const ext = format;
10955
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
11552
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
10956
11553
  await Bun.write(screenshotPath, finalBuffer);
10957
11554
  let thumbnailPath;
10958
11555
  let thumbnailBase64;
@@ -11012,12 +11609,12 @@ async function takeScreenshot(page, opts) {
11012
11609
  }
11013
11610
  async function generatePDF(page, opts) {
11014
11611
  try {
11015
- const base = join3(getDataDir2(), "pdfs");
11612
+ const base = join4(getDataDir2(), "pdfs");
11016
11613
  const date = new Date().toISOString().split("T")[0];
11017
- const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
11018
- mkdirSync3(dir, { recursive: true });
11614
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
11615
+ mkdirSync4(dir, { recursive: true });
11019
11616
  const timestamp = Date.now();
11020
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
11617
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
11021
11618
  const buffer = await page.pdf({
11022
11619
  path: pdfPath,
11023
11620
  format: opts?.format ?? "A4",
@@ -11344,6 +11941,17 @@ var init_recordings = __esm(() => {
11344
11941
  });
11345
11942
 
11346
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
+ });
11347
11955
  function startRecording(sessionId, name, startUrl) {
11348
11956
  const steps = [];
11349
11957
  const recording = createRecording({ name, start_url: startUrl, steps });
@@ -11354,6 +11962,23 @@ function startRecording(sessionId, name, startUrl) {
11354
11962
  });
11355
11963
  return recording;
11356
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
+ }
11357
11982
  function recordStep(recordingId, step) {
11358
11983
  const active = activeRecordings.get(recordingId);
11359
11984
  if (!active)
@@ -11425,6 +12050,64 @@ async function replayRecording(recordingId, page) {
11425
12050
  duration_ms: Date.now() - startTime
11426
12051
  };
11427
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
+ }
11428
12111
  var activeRecordings;
11429
12112
  var init_recorder = __esm(() => {
11430
12113
  init_recordings();
@@ -15399,118 +16082,6 @@ var init_zod = __esm(() => {
15399
16082
  init_external();
15400
16083
  });
15401
16084
 
15402
- // src/engines/cdp.ts
15403
- class CDPClient {
15404
- session;
15405
- networkEnabled = false;
15406
- performanceEnabled = false;
15407
- constructor(session) {
15408
- this.session = session;
15409
- }
15410
- static async fromPage(page) {
15411
- try {
15412
- const session = await page.context().newCDPSession(page);
15413
- return new CDPClient(session);
15414
- } catch (err) {
15415
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
15416
- }
15417
- }
15418
- async send(method, params) {
15419
- try {
15420
- return await this.session.send(method, params);
15421
- } catch (err) {
15422
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
15423
- }
15424
- }
15425
- on(event, handler) {
15426
- this.session.on(event, handler);
15427
- }
15428
- off(event, handler) {
15429
- this.session.off(event, handler);
15430
- }
15431
- async enableNetwork() {
15432
- if (!this.networkEnabled) {
15433
- await this.send("Network.enable");
15434
- this.networkEnabled = true;
15435
- }
15436
- }
15437
- async enablePerformance() {
15438
- if (!this.performanceEnabled) {
15439
- await this.send("Performance.enable");
15440
- this.performanceEnabled = true;
15441
- }
15442
- }
15443
- async getPerformanceMetrics() {
15444
- await this.enablePerformance();
15445
- const result = await this.send("Performance.getMetrics");
15446
- const m = {};
15447
- for (const metric of result.metrics) {
15448
- m[metric.name] = metric.value;
15449
- }
15450
- return {
15451
- js_heap_size_used: m["JSHeapUsedSize"],
15452
- js_heap_size_total: m["JSHeapTotalSize"],
15453
- dom_interactive: m["DOMInteractive"],
15454
- dom_complete: m["DOMComplete"],
15455
- load_event: m["LoadEventEnd"]
15456
- };
15457
- }
15458
- async startJSCoverage() {
15459
- await this.send("Profiler.enable");
15460
- await this.send("Debugger.enable");
15461
- await this.send("Profiler.startPreciseCoverage", {
15462
- callCount: false,
15463
- detailed: true
15464
- });
15465
- }
15466
- async stopJSCoverage() {
15467
- const result = await this.send("Profiler.takePreciseCoverage");
15468
- await this.send("Profiler.stopPreciseCoverage");
15469
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
15470
- url: r.url,
15471
- text: "",
15472
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
15473
- }));
15474
- }
15475
- async getCoverage() {
15476
- await this.startJSCoverage();
15477
- const js = await this.stopJSCoverage();
15478
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
15479
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
15480
- }
15481
- async captureHAREntries(page, handler) {
15482
- await this.enableNetwork();
15483
- const requestTimings = new Map;
15484
- const onRequest = (params) => {
15485
- requestTimings.set(params.requestId, params.timestamp);
15486
- };
15487
- const onResponse = (params) => {
15488
- const start = requestTimings.get(params.requestId);
15489
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
15490
- handler({
15491
- method: "GET",
15492
- url: params.response.url,
15493
- status: params.response.status,
15494
- duration
15495
- });
15496
- };
15497
- this.on("Network.requestWillBeSent", onRequest);
15498
- this.on("Network.responseReceived", onResponse);
15499
- return () => {
15500
- this.off("Network.requestWillBeSent", onRequest);
15501
- this.off("Network.responseReceived", onResponse);
15502
- };
15503
- }
15504
- async detach() {
15505
- try {
15506
- await this.session.detach();
15507
- } catch {}
15508
- }
15509
- }
15510
- var init_cdp = __esm(() => {
15511
- init_types();
15512
- });
15513
-
15514
16085
  // src/lib/performance.ts
15515
16086
  async function getPerformanceMetrics(page) {
15516
16087
  const navTiming = await page.evaluate(() => {
@@ -15616,16 +16187,16 @@ __export(exports_downloads, {
15616
16187
  cleanStaleDownloads: () => cleanStaleDownloads
15617
16188
  });
15618
16189
  import { randomUUID as randomUUID9 } from "crypto";
15619
- import { join as join4, basename, extname } from "path";
15620
- import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
15621
- 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";
15622
16193
  function getDataDir3() {
15623
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
16194
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15624
16195
  }
15625
16196
  function getDownloadsDir(sessionId) {
15626
- const base = join4(getDataDir3(), "downloads");
15627
- const dir = sessionId ? join4(base, sessionId) : base;
15628
- mkdirSync4(dir, { recursive: true });
16197
+ const base = join5(getDataDir3(), "downloads");
16198
+ const dir = sessionId ? join5(base, sessionId) : base;
16199
+ mkdirSync5(dir, { recursive: true });
15629
16200
  return dir;
15630
16201
  }
15631
16202
  function ensureDownloadsDir() {
@@ -15640,7 +16211,7 @@ function saveToDownloads(buffer, filename, opts) {
15640
16211
  const ext = extname(filename) || "";
15641
16212
  const stem = basename(filename, ext);
15642
16213
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
15643
- const filePath = join4(dir, uniqueName);
16214
+ const filePath = join5(dir, uniqueName);
15644
16215
  writeFileSync(filePath, buffer);
15645
16216
  const meta = {
15646
16217
  id,
@@ -15669,20 +16240,20 @@ function listDownloads(sessionId) {
15669
16240
  const dir = getDownloadsDir(sessionId);
15670
16241
  const results = [];
15671
16242
  function scanDir(d) {
15672
- if (!existsSync(d))
16243
+ if (!existsSync2(d))
15673
16244
  return;
15674
- const entries = readdirSync(d);
16245
+ const entries = readdirSync2(d);
15675
16246
  for (const entry of entries) {
15676
16247
  if (entry.endsWith(".meta.json"))
15677
16248
  continue;
15678
- const full = join4(d, entry);
16249
+ const full = join5(d, entry);
15679
16250
  const stat = statSync(full);
15680
16251
  if (stat.isDirectory()) {
15681
16252
  scanDir(full);
15682
16253
  continue;
15683
16254
  }
15684
16255
  const mpath = metaPath(full);
15685
- if (!existsSync(mpath))
16256
+ if (!existsSync2(mpath))
15686
16257
  continue;
15687
16258
  try {
15688
16259
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -15712,9 +16283,9 @@ function deleteDownload(id, sessionId) {
15712
16283
  if (!file)
15713
16284
  return false;
15714
16285
  try {
15715
- unlinkSync(file.path);
15716
- if (existsSync(file.meta_path))
15717
- unlinkSync(file.meta_path);
16286
+ unlinkSync2(file.path);
16287
+ if (existsSync2(file.meta_path))
16288
+ unlinkSync2(file.meta_path);
15718
16289
  return true;
15719
16290
  } catch {
15720
16291
  return false;
@@ -15764,9 +16335,9 @@ var exports_gallery_diff = {};
15764
16335
  __export(exports_gallery_diff, {
15765
16336
  diffImages: () => diffImages
15766
16337
  });
15767
- import { join as join5 } from "path";
15768
- import { mkdirSync as mkdirSync5 } from "fs";
15769
- 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";
15770
16341
  async function diffImages(path1, path2) {
15771
16342
  const img1 = import_sharp2.default(path1);
15772
16343
  const img2 = import_sharp2.default(path2);
@@ -15797,10 +16368,10 @@ async function diffImages(path1, path2) {
15797
16368
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
15798
16369
  }
15799
16370
  }
15800
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15801
- const diffDir = join5(dataDir, "diffs");
15802
- mkdirSync5(diffDir, { recursive: true });
15803
- 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`);
15804
16375
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
15805
16376
  await Bun.write(diffPath, diffImageBuffer);
15806
16377
  return {
@@ -15817,9 +16388,9 @@ var init_gallery_diff = __esm(() => {
15817
16388
  });
15818
16389
 
15819
16390
  // src/lib/files-integration.ts
15820
- import { join as join6 } from "path";
15821
- import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
15822
- 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";
15823
16394
  async function persistFile(localPath, opts) {
15824
16395
  try {
15825
16396
  const mod = await import("@hasna/files");
@@ -15828,12 +16399,12 @@ async function persistFile(localPath, opts) {
15828
16399
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
15829
16400
  }
15830
16401
  } catch {}
15831
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
16402
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
15832
16403
  const date = new Date().toISOString().split("T")[0];
15833
- const dir = join6(dataDir, "persistent", date);
15834
- mkdirSync6(dir, { recursive: true });
16404
+ const dir = join7(dataDir, "persistent", date);
16405
+ mkdirSync7(dir, { recursive: true });
15835
16406
  const filename = localPath.split("/").pop() ?? "file";
15836
- const targetPath = join6(dir, filename);
16407
+ const targetPath = join7(dir, filename);
15837
16408
  copyFileSync2(localPath, targetPath);
15838
16409
  return {
15839
16410
  id: `local-${Date.now()}`,
@@ -15844,6 +16415,20 @@ async function persistFile(localPath, opts) {
15844
16415
  }
15845
16416
  var init_files_integration = () => {};
15846
16417
 
16418
+ // src/db/timeline.ts
16419
+ function logEvent(sessionId, eventType, details = {}) {
16420
+ const db = getDatabase();
16421
+ const id = crypto.randomUUID();
16422
+ db.prepare("INSERT INTO session_events (id, session_id, event_type, details) VALUES (?, ?, ?, ?)").run(id, sessionId, eventType, JSON.stringify(details));
16423
+ }
16424
+ function getTimeline(sessionId, limit = 100) {
16425
+ const db = getDatabase();
16426
+ return db.query("SELECT * FROM session_events WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?").all(sessionId, limit);
16427
+ }
16428
+ var init_timeline = __esm(() => {
16429
+ init_schema();
16430
+ });
16431
+
15847
16432
  // src/lib/tabs.ts
15848
16433
  async function newTab(page, url) {
15849
16434
  const context = page.context();
@@ -15934,23 +16519,23 @@ __export(exports_profiles, {
15934
16519
  deleteProfile: () => deleteProfile,
15935
16520
  applyProfile: () => applyProfile
15936
16521
  });
15937
- import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
15938
- import { join as join7 } from "path";
15939
- 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";
15940
16525
  function getProfilesDir() {
15941
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
15942
- const dir = join7(dataDir, "profiles");
15943
- 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 });
15944
16529
  return dir;
15945
16530
  }
15946
16531
  function getProfileDir2(name) {
15947
- return join7(getProfilesDir(), name);
16532
+ return join8(getProfilesDir(), name);
15948
16533
  }
15949
16534
  async function saveProfile(page, name) {
15950
16535
  const dir = getProfileDir2(name);
15951
- mkdirSync7(dir, { recursive: true });
16536
+ mkdirSync8(dir, { recursive: true });
15952
16537
  const cookies = await page.context().cookies();
15953
- writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
16538
+ writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
15954
16539
  let localStorage2 = {};
15955
16540
  try {
15956
16541
  localStorage2 = await page.evaluate(() => {
@@ -15962,11 +16547,11 @@ async function saveProfile(page, name) {
15962
16547
  return result;
15963
16548
  });
15964
16549
  } catch {}
15965
- writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
16550
+ writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
15966
16551
  const savedAt = new Date().toISOString();
15967
16552
  const url = page.url();
15968
16553
  const meta = { saved_at: savedAt, url };
15969
- writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
16554
+ writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
15970
16555
  return {
15971
16556
  name,
15972
16557
  saved_at: savedAt,
@@ -15977,17 +16562,17 @@ async function saveProfile(page, name) {
15977
16562
  }
15978
16563
  function loadProfile(name) {
15979
16564
  const dir = getProfileDir2(name);
15980
- if (!existsSync3(dir)) {
16565
+ if (!existsSync4(dir)) {
15981
16566
  throw new Error(`Profile not found: ${name}`);
15982
16567
  }
15983
- const cookiesPath = join7(dir, "cookies.json");
15984
- const storagePath = join7(dir, "storage.json");
15985
- const metaPath2 = join7(dir, "meta.json");
15986
- const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
15987
- 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")) : {};
15988
16573
  let savedAt = new Date().toISOString();
15989
16574
  let url;
15990
- if (existsSync3(metaPath2)) {
16575
+ if (existsSync4(metaPath2)) {
15991
16576
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
15992
16577
  savedAt = meta.saved_at ?? savedAt;
15993
16578
  url = meta.url;
@@ -16015,33 +16600,33 @@ async function applyProfile(page, profileData) {
16015
16600
  }
16016
16601
  function listProfiles() {
16017
16602
  const dir = getProfilesDir();
16018
- if (!existsSync3(dir))
16603
+ if (!existsSync4(dir))
16019
16604
  return [];
16020
- const entries = readdirSync2(dir, { withFileTypes: true });
16605
+ const entries = readdirSync3(dir, { withFileTypes: true });
16021
16606
  const profiles = [];
16022
16607
  for (const entry of entries) {
16023
16608
  if (!entry.isDirectory())
16024
16609
  continue;
16025
16610
  const name = entry.name;
16026
- const profileDir = join7(dir, name);
16611
+ const profileDir = join8(dir, name);
16027
16612
  let savedAt = "";
16028
16613
  let url;
16029
16614
  let cookieCount = 0;
16030
16615
  let storageKeyCount = 0;
16031
16616
  try {
16032
- const metaPath2 = join7(profileDir, "meta.json");
16033
- if (existsSync3(metaPath2)) {
16617
+ const metaPath2 = join8(profileDir, "meta.json");
16618
+ if (existsSync4(metaPath2)) {
16034
16619
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
16035
16620
  savedAt = meta.saved_at ?? "";
16036
16621
  url = meta.url;
16037
16622
  }
16038
- const cookiesPath = join7(profileDir, "cookies.json");
16039
- if (existsSync3(cookiesPath)) {
16623
+ const cookiesPath = join8(profileDir, "cookies.json");
16624
+ if (existsSync4(cookiesPath)) {
16040
16625
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
16041
16626
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
16042
16627
  }
16043
- const storagePath = join7(profileDir, "storage.json");
16044
- if (existsSync3(storagePath)) {
16628
+ const storagePath = join8(profileDir, "storage.json");
16629
+ if (existsSync4(storagePath)) {
16045
16630
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
16046
16631
  storageKeyCount = Object.keys(storage).length;
16047
16632
  }
@@ -16058,7 +16643,7 @@ function listProfiles() {
16058
16643
  }
16059
16644
  function deleteProfile(name) {
16060
16645
  const dir = getProfileDir2(name);
16061
- if (!existsSync3(dir))
16646
+ if (!existsSync4(dir))
16062
16647
  return false;
16063
16648
  try {
16064
16649
  rmSync(dir, { recursive: true, force: true });
@@ -16069,6 +16654,97 @@ function deleteProfile(name) {
16069
16654
  }
16070
16655
  var init_profiles = () => {};
16071
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
+
16072
16748
  // src/lib/annotate.ts
16073
16749
  var exports_annotate = {};
16074
16750
  __export(exports_annotate, {
@@ -16129,26 +16805,213 @@ var init_annotate = __esm(() => {
16129
16805
  import_sharp3 = __toESM(require_lib(), 1);
16130
16806
  });
16131
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
+
16132
16995
  // src/lib/auth.ts
16133
16996
  var exports_auth = {};
16134
16997
  __export(exports_auth, {
16135
16998
  loginWithCredentials: () => loginWithCredentials,
16136
16999
  getCredentials: () => getCredentials
16137
17000
  });
16138
- import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
16139
- import { join as join8 } from "path";
16140
- 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";
16141
17004
  async function getCredentials(service) {
16142
17005
  try {
16143
- 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`);
16144
17007
  const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
16145
17008
  const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
16146
17009
  if (email?.value && password?.value) {
16147
17010
  return { email: email.value, password: password.value };
16148
17011
  }
16149
17012
  } catch {}
16150
- const secretsPath = join8(homedir8(), ".secrets");
16151
- if (existsSync4(secretsPath)) {
17013
+ const secretsPath = join9(homedir9(), ".secrets");
17014
+ if (existsSync5(secretsPath)) {
16152
17015
  const content = readFileSync3(secretsPath, "utf8");
16153
17016
  const lines = content.split(`
16154
17017
  `);
@@ -16235,6 +17098,7 @@ __export(exports_dist, {
16235
17098
  shortUuid: () => shortUuid,
16236
17099
  setFocus: () => setFocus,
16237
17100
  setActiveProfile: () => setActiveProfile,
17101
+ setActiveModel: () => setActiveModel,
16238
17102
  searchMemories: () => searchMemories,
16239
17103
  runCleanup: () => runCleanup,
16240
17104
  resolveProjectId: () => resolveProjectId,
@@ -16285,6 +17149,8 @@ __export(exports_dist, {
16285
17149
  getAutoMemoryStats: () => getAutoMemoryStats,
16286
17150
  getAgent: () => getAgent2,
16287
17151
  getActiveProfile: () => getActiveProfile,
17152
+ getActiveModel: () => getActiveModel,
17153
+ gatherTrainingData: () => gatherTrainingData,
16288
17154
  focusFilterSQL: () => focusFilterSQL,
16289
17155
  findPath: () => findPath,
16290
17156
  enforceQuotas: () => enforceQuotas,
@@ -16301,6 +17167,7 @@ __export(exports_dist, {
16301
17167
  containsSecrets: () => containsSecrets,
16302
17168
  configureAutoMemory: () => configureAutoMemory,
16303
17169
  closeDatabase: () => closeDatabase,
17170
+ clearActiveModel: () => clearActiveModel,
16304
17171
  cleanExpiredMemories: () => cleanExpiredMemories,
16305
17172
  cleanExpiredLocks: () => cleanExpiredLocks,
16306
17173
  checkMemoryWriteLock: () => checkMemoryWriteLock,
@@ -16321,25 +17188,29 @@ __export(exports_dist, {
16321
17188
  InvalidScopeError: () => InvalidScopeError,
16322
17189
  EntityNotFoundError: () => EntityNotFoundError,
16323
17190
  DuplicateMemoryError: () => DuplicateMemoryError,
17191
+ DEFAULT_MODEL: () => DEFAULT_MODEL2,
16324
17192
  DEFAULT_CONFIG: () => DEFAULT_CONFIG
16325
17193
  });
16326
17194
  import { Database as Database2 } from "bun:sqlite";
16327
- import { existsSync as existsSync5, mkdirSync as mkdirSync8 } from "fs";
16328
- import { dirname, join as join9, resolve } from "path";
16329
- import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
16330
- 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";
16331
17199
  import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
16332
17200
  import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
16333
17201
  import { homedir as homedir22 } from "os";
16334
17202
  import { join as join32 } from "path";
17203
+ import { existsSync as existsSync42, mkdirSync as mkdirSync42, readFileSync as readFileSync32, writeFileSync as writeFileSync32 } from "fs";
17204
+ import { homedir as homedir32 } from "os";
17205
+ import { join as join42 } from "path";
16335
17206
  function isInMemoryDb(path) {
16336
17207
  return path === ":memory:" || path.startsWith("file::memory:");
16337
17208
  }
16338
17209
  function findNearestMementosDb(startDir) {
16339
17210
  let dir = resolve(startDir);
16340
17211
  while (true) {
16341
- const candidate = join9(dir, ".mementos", "mementos.db");
16342
- if (existsSync5(candidate))
17212
+ const candidate = join10(dir, ".mementos", "mementos.db");
17213
+ if (existsSync6(candidate))
16343
17214
  return candidate;
16344
17215
  const parent = dirname(dir);
16345
17216
  if (parent === dir)
@@ -16351,7 +17222,7 @@ function findNearestMementosDb(startDir) {
16351
17222
  function findGitRoot(startDir) {
16352
17223
  let dir = resolve(startDir);
16353
17224
  while (true) {
16354
- if (existsSync5(join9(dir, ".git")))
17225
+ if (existsSync6(join10(dir, ".git")))
16355
17226
  return dir;
16356
17227
  const parent = dirname(dir);
16357
17228
  if (parent === dir)
@@ -16371,25 +17242,25 @@ function getDbPath() {
16371
17242
  if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
16372
17243
  const gitRoot = findGitRoot(cwd);
16373
17244
  if (gitRoot) {
16374
- return join9(gitRoot, ".mementos", "mementos.db");
17245
+ return join10(gitRoot, ".mementos", "mementos.db");
16375
17246
  }
16376
17247
  }
16377
17248
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
16378
- return join9(home, ".mementos", "mementos.db");
17249
+ return join10(home, ".mementos", "mementos.db");
16379
17250
  }
16380
- function ensureDir(filePath) {
17251
+ function ensureDir2(filePath) {
16381
17252
  if (isInMemoryDb(filePath))
16382
17253
  return;
16383
17254
  const dir = dirname(resolve(filePath));
16384
- if (!existsSync5(dir)) {
16385
- mkdirSync8(dir, { recursive: true });
17255
+ if (!existsSync6(dir)) {
17256
+ mkdirSync9(dir, { recursive: true });
16386
17257
  }
16387
17258
  }
16388
17259
  function getDatabase2(dbPath) {
16389
17260
  if (_db2)
16390
17261
  return _db2;
16391
17262
  const path = dbPath || getDbPath();
16392
- ensureDir(path);
17263
+ ensureDir2(path);
16393
17264
  _db2 = new Database2(path, { create: true });
16394
17265
  _db2.run("PRAGMA journal_mode = WAL");
16395
17266
  _db2.run("PRAGMA busy_timeout = 5000");
@@ -18232,7 +19103,7 @@ function isValidCategory(value) {
18232
19103
  return VALID_CATEGORIES.includes(value);
18233
19104
  }
18234
19105
  function loadConfig() {
18235
- const configPath = join22(homedir9(), ".mementos", "config.json");
19106
+ const configPath = join22(homedir10(), ".mementos", "config.json");
18236
19107
  let fileConfig = {};
18237
19108
  if (existsSync22(configPath)) {
18238
19109
  try {
@@ -18259,10 +19130,10 @@ function loadConfig() {
18259
19130
  return merged;
18260
19131
  }
18261
19132
  function profilesDir() {
18262
- return join22(homedir9(), ".mementos", "profiles");
19133
+ return join22(homedir10(), ".mementos", "profiles");
18263
19134
  }
18264
19135
  function globalConfigPath() {
18265
- return join22(homedir9(), ".mementos", "config.json");
19136
+ return join22(homedir10(), ".mementos", "config.json");
18266
19137
  }
18267
19138
  function readGlobalConfig() {
18268
19139
  const p = globalConfigPath();
@@ -18276,7 +19147,7 @@ function readGlobalConfig() {
18276
19147
  }
18277
19148
  function writeGlobalConfig(data) {
18278
19149
  const p = globalConfigPath();
18279
- ensureDir2(dirname2(p));
19150
+ ensureDir22(dirname2(p));
18280
19151
  writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
18281
19152
  }
18282
19153
  function getActiveProfile() {
@@ -18299,18 +19170,18 @@ function listProfiles2() {
18299
19170
  const dir = profilesDir();
18300
19171
  if (!existsSync22(dir))
18301
19172
  return [];
18302
- 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();
18303
19174
  }
18304
19175
  function deleteProfile2(name) {
18305
19176
  const dbPath = join22(profilesDir(), `${name}.db`);
18306
19177
  if (!existsSync22(dbPath))
18307
19178
  return false;
18308
- unlinkSync2(dbPath);
19179
+ unlinkSync3(dbPath);
18309
19180
  if (getActiveProfile() === name)
18310
19181
  setActiveProfile(null);
18311
19182
  return true;
18312
19183
  }
18313
- function ensureDir2(dir) {
19184
+ function ensureDir22(dir) {
18314
19185
  if (!existsSync22(dir)) {
18315
19186
  mkdirSync22(dir, { recursive: true });
18316
19187
  }
@@ -19365,6 +20236,85 @@ function jaccardSimilarity(a, b) {
19365
20236
  const union = new Set([...a, ...b]).size;
19366
20237
  return intersection / union;
19367
20238
  }
20239
+ function memoryToRecallExample(memory) {
20240
+ return {
20241
+ messages: [
20242
+ { role: "system", content: SYSTEM_PROMPT },
20243
+ {
20244
+ role: "user",
20245
+ content: `What do you remember about "${memory.key}"?`
20246
+ },
20247
+ {
20248
+ role: "assistant",
20249
+ content: memory.summary ? `${memory.value}
20250
+
20251
+ Summary: ${memory.summary}` : memory.value
20252
+ }
20253
+ ]
20254
+ };
20255
+ }
20256
+ function memoryToSaveExample(memory) {
20257
+ const tags = memory.tags ?? [];
20258
+ return {
20259
+ messages: [
20260
+ { role: "system", content: SYSTEM_PROMPT },
20261
+ {
20262
+ role: "user",
20263
+ content: `Remember this for me: ${memory.key} = ${memory.value}${tags.length ? ` (tags: ${tags.join(", ")})` : ""}`
20264
+ },
20265
+ {
20266
+ role: "assistant",
20267
+ content: `Saved to memory: "${memory.key}" with ${memory.category} category, importance ${memory.importance}/10, scope: ${memory.scope}.`
20268
+ }
20269
+ ]
20270
+ };
20271
+ }
20272
+ function memoryToSearchExample(memories, category) {
20273
+ const matched = memories.filter((m) => m.category === category && m.status === "active").slice(0, 5);
20274
+ return {
20275
+ messages: [
20276
+ { role: "system", content: SYSTEM_PROMPT },
20277
+ { role: "user", content: `What ${category} memories do you have?` },
20278
+ {
20279
+ role: "assistant",
20280
+ content: matched.length > 0 ? `Here are my ${category} memories:
20281
+ ${matched.map((m) => `- ${m.key}: ${m.value.slice(0, 120)}${m.value.length > 120 ? "..." : ""}`).join(`
20282
+ `)}` : `I don't have any ${category} memories stored yet.`
20283
+ }
20284
+ ]
20285
+ };
20286
+ }
20287
+ function readConfig() {
20288
+ if (!existsSync42(CONFIG_PATH))
20289
+ return {};
20290
+ try {
20291
+ const raw = readFileSync32(CONFIG_PATH, "utf-8");
20292
+ return JSON.parse(raw);
20293
+ } catch {
20294
+ return {};
20295
+ }
20296
+ }
20297
+ function writeConfig(config) {
20298
+ if (!existsSync42(CONFIG_DIR)) {
20299
+ mkdirSync42(CONFIG_DIR, { recursive: true });
20300
+ }
20301
+ writeFileSync32(CONFIG_PATH, JSON.stringify(config, null, 2) + `
20302
+ `, "utf-8");
20303
+ }
20304
+ function getActiveModel() {
20305
+ const config = readConfig();
20306
+ return config.activeModel ?? DEFAULT_MODEL2;
20307
+ }
20308
+ function setActiveModel(modelId) {
20309
+ const config = readConfig();
20310
+ config.activeModel = modelId;
20311
+ writeConfig(config);
20312
+ }
20313
+ function clearActiveModel() {
20314
+ const config = readConfig();
20315
+ delete config.activeModel;
20316
+ writeConfig(config);
20317
+ }
19368
20318
  var __defProp2, __export2 = (target, all) => {
19369
20319
  for (var name in all)
19370
20320
  __defProp2(target, name, {
@@ -19407,7 +20357,27 @@ Return JSON with this exact shape:
19407
20357
  "relations": [
19408
20358
  { "from": string, "to": string, "type": "uses"|"knows"|"depends_on"|"created_by"|"related_to"|"contradicts"|"part_of"|"implements"|"happened_before"|"happened_after"|"caused_by"|"resulted_in"|"supersedes"|"version_of" }
19409
20359
  ]
19410
- }`, ANTHROPIC_MODELS, AnthropicProvider, OpenAICompatProvider, OPENAI_MODELS, OpenAIProvider, CEREBRAS_MODELS, CerebrasProvider, GROK_MODELS, GrokProvider, providerRegistry, MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue, DEDUP_SIMILARITY_THRESHOLD = 0.85, DEFAULT_CONFIG2, _stats;
20360
+ }`, ANTHROPIC_MODELS, AnthropicProvider, OpenAICompatProvider, OPENAI_MODELS, OpenAIProvider, CEREBRAS_MODELS, CerebrasProvider, GROK_MODELS, GrokProvider, providerRegistry, MAX_QUEUE_SIZE = 100, CONCURRENCY = 3, autoMemoryQueue, DEDUP_SIMILARITY_THRESHOLD = 0.85, DEFAULT_CONFIG2, _stats, SYSTEM_PROMPT = "You are an AI assistant with persistent memory that recalls and saves information across sessions.", gatherTrainingData = async (options = {}) => {
20361
+ const allMemories = listMemories({ status: "active" });
20362
+ const filtered = options.since ? allMemories.filter((m) => new Date(m.created_at) >= options.since) : allMemories;
20363
+ const sorted = filtered.slice().sort((a, b) => b.importance - a.importance);
20364
+ const fetchSet = options.limit ? sorted.slice(0, options.limit * 3) : sorted;
20365
+ const examples = [];
20366
+ for (const memory of fetchSet) {
20367
+ examples.push(memoryToRecallExample(memory));
20368
+ examples.push(memoryToSaveExample(memory));
20369
+ }
20370
+ const categories = [...new Set(fetchSet.map((m) => m.category))];
20371
+ for (const category of categories) {
20372
+ examples.push(memoryToSearchExample(fetchSet, category));
20373
+ }
20374
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
20375
+ return {
20376
+ source: "mementos",
20377
+ examples: finalExamples,
20378
+ count: finalExamples.length
20379
+ };
20380
+ }, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
19411
20381
  var init_dist = __esm(() => {
19412
20382
  __defProp2 = Object.defineProperty;
19413
20383
  exports_database = {};
@@ -20687,6 +21657,8 @@ Return only a number 0-10.`);
20687
21657
  keepLonger: true
20688
21658
  };
20689
21659
  _stats = { checked: 0, skipped: 0, updated: 0 };
21660
+ CONFIG_DIR = join42(homedir32(), ".mementos");
21661
+ CONFIG_PATH = join42(CONFIG_DIR, "config.json");
20690
21662
  });
20691
21663
 
20692
21664
  // src/lib/page-memory.ts
@@ -20841,30 +21813,30 @@ __export(exports_dist2, {
20841
21813
  acquireLock: () => acquireLock2
20842
21814
  });
20843
21815
  import { Database as Database3 } from "bun:sqlite";
20844
- import { mkdirSync as mkdirSync9 } from "fs";
20845
- import { join as join10, dirname as dirname3 } from "path";
20846
- import { homedir as homedir10 } from "os";
20847
- 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";
20848
21820
  import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
20849
21821
  import { join as join33 } from "path";
20850
- import { homedir as homedir32 } from "os";
21822
+ import { homedir as homedir33 } from "os";
20851
21823
  import { readFileSync as readFileSync5 } from "fs";
20852
21824
  import { join as join23 } from "path";
20853
21825
  import { homedir as homedir23 } from "os";
20854
21826
  import { randomUUID as randomUUID22 } from "crypto";
20855
21827
  import { readFileSync as readFileSync23, writeFileSync as writeFileSync4, mkdirSync as mkdirSync33 } from "fs";
20856
- import { join as join42, dirname as dirname22 } from "path";
21828
+ import { join as join43, dirname as dirname22 } from "path";
20857
21829
  import { homedir as homedir42 } from "os";
20858
21830
  function getDbPath2() {
20859
21831
  if (process.env.CONVERSATIONS_DB_PATH)
20860
21832
  return process.env.CONVERSATIONS_DB_PATH;
20861
- return join10(homedir10(), ".conversations", "messages.db");
21833
+ return join11(homedir11(), ".conversations", "messages.db");
20862
21834
  }
20863
21835
  function getDb() {
20864
21836
  if (db)
20865
21837
  return db;
20866
21838
  const dbPath = getDbPath2();
20867
- mkdirSync9(dirname3(dbPath), { recursive: true });
21839
+ mkdirSync10(dirname3(dbPath), { recursive: true });
20868
21840
  db = new Database3(dbPath, { create: true });
20869
21841
  db.exec("PRAGMA journal_mode = WAL");
20870
21842
  db.exec("PRAGMA busy_timeout = 5000");
@@ -21192,7 +22164,7 @@ function parseMessage(row) {
21192
22164
  function getAttachmentsDir() {
21193
22165
  if (process.env.CONVERSATIONS_ATTACHMENTS_DIR)
21194
22166
  return process.env.CONVERSATIONS_ATTACHMENTS_DIR;
21195
- return join33(homedir32(), ".conversations", "attachments");
22167
+ return join33(homedir33(), ".conversations", "attachments");
21196
22168
  }
21197
22169
  function guessMimeType(name) {
21198
22170
  const ext = name.split(".").pop()?.toLowerCase();
@@ -21224,7 +22196,7 @@ function guessMimeType(name) {
21224
22196
  function sendMessage(opts) {
21225
22197
  const db2 = getDb();
21226
22198
  const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
21227
- 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)}`);
21228
22200
  const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
21229
22201
  const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
21230
22202
  const blocking = opts.blocking ? 1 : 0;
@@ -24863,7 +25835,7 @@ Check the top-level render call using <` + parentName + ">.";
24863
25835
  "zinc-eagle",
24864
25836
  "zone-fox"
24865
25837
  ];
24866
- AGENT_ID_FILE = join42(homedir42(), ".conversations", "agent-id");
25838
+ AGENT_ID_FILE = join43(homedir42(), ".conversations", "agent-id");
24867
25839
  init_db();
24868
25840
  init_db();
24869
25841
  CONFLICT_THRESHOLD_SECONDS = 30 * 60;
@@ -25205,7 +26177,7 @@ __export(exports_dist3, {
25205
26177
  deleteTemplate: () => deleteTemplate,
25206
26178
  deleteTaskList: () => deleteTaskList,
25207
26179
  deleteTask: () => deleteTask,
25208
- deleteSession: () => deleteSession,
26180
+ deleteSession: () => deleteSession2,
25209
26181
  deleteProject: () => deleteProject2,
25210
26182
  deletePlan: () => deletePlan,
25211
26183
  deleteOrg: () => deleteOrg,
@@ -25266,17 +26238,17 @@ __export(exports_dist3, {
25266
26238
  AgentNotFoundError: () => AgentNotFoundError2
25267
26239
  });
25268
26240
  import { Database as Database4 } from "bun:sqlite";
25269
- import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
25270
- 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";
25271
26243
  import { existsSync as existsSync33 } from "fs";
25272
26244
  import { join as join34 } from "path";
25273
- 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";
25274
26246
  import { join as join24 } from "path";
25275
- import { existsSync as existsSync42, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
25276
- import { join as join43 } from "path";
26247
+ import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
26248
+ import { join as join44 } from "path";
25277
26249
  import { existsSync as existsSync52 } from "fs";
25278
26250
  import { join as join52 } from "path";
25279
- import { readFileSync as readFileSync32, statSync as statSync22 } from "fs";
26251
+ import { readFileSync as readFileSync33, statSync as statSync22 } from "fs";
25280
26252
  import { relative, resolve as resolve22, join as join62 } from "path";
25281
26253
  import { execSync as execSync2 } from "child_process";
25282
26254
 
@@ -25500,8 +26472,8 @@ function isInMemoryDb2(path) {
25500
26472
  function findNearestTodosDb(startDir) {
25501
26473
  let dir = resolve3(startDir);
25502
26474
  while (true) {
25503
- const candidate = join11(dir, ".todos", "todos.db");
25504
- if (existsSync6(candidate))
26475
+ const candidate = join12(dir, ".todos", "todos.db");
26476
+ if (existsSync7(candidate))
25505
26477
  return candidate;
25506
26478
  const parent = dirname5(dir);
25507
26479
  if (parent === dir)
@@ -25513,7 +26485,7 @@ function findNearestTodosDb(startDir) {
25513
26485
  function findGitRoot2(startDir) {
25514
26486
  let dir = resolve3(startDir);
25515
26487
  while (true) {
25516
- if (existsSync6(join11(dir, ".git")))
26488
+ if (existsSync7(join12(dir, ".git")))
25517
26489
  return dir;
25518
26490
  const parent = dirname5(dir);
25519
26491
  if (parent === dir)
@@ -25533,18 +26505,18 @@ function getDbPath3() {
25533
26505
  if (process.env["TODOS_DB_SCOPE"] === "project") {
25534
26506
  const gitRoot = findGitRoot2(cwd);
25535
26507
  if (gitRoot) {
25536
- return join11(gitRoot, ".todos", "todos.db");
26508
+ return join12(gitRoot, ".todos", "todos.db");
25537
26509
  }
25538
26510
  }
25539
26511
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
25540
- return join11(home, ".todos", "todos.db");
26512
+ return join12(home, ".todos", "todos.db");
25541
26513
  }
25542
26514
  function ensureDir3(filePath) {
25543
26515
  if (isInMemoryDb2(filePath))
25544
26516
  return;
25545
26517
  const dir = dirname5(resolve3(filePath));
25546
- if (!existsSync6(dir)) {
25547
- mkdirSync10(dir, { recursive: true });
26518
+ if (!existsSync7(dir)) {
26519
+ mkdirSync11(dir, { recursive: true });
25548
26520
  }
25549
26521
  }
25550
26522
  function getDatabase3(dbPath) {
@@ -26003,14 +26975,14 @@ function ensureProject2(name, path, db2) {
26003
26975
  }
26004
26976
  return createProject3({ name, path }, d);
26005
26977
  }
26006
- function ensureDir22(dir) {
26978
+ function ensureDir23(dir) {
26007
26979
  if (!existsSync23(dir))
26008
26980
  mkdirSync24(dir, { recursive: true });
26009
26981
  }
26010
26982
  function listJsonFiles(dir) {
26011
26983
  if (!existsSync23(dir))
26012
26984
  return [];
26013
- return readdirSync4(dir).filter((f) => f.endsWith(".json"));
26985
+ return readdirSync5(dir).filter((f) => f.endsWith(".json"));
26014
26986
  }
26015
26987
  function readJsonFile(path) {
26016
26988
  try {
@@ -27634,11 +28606,11 @@ function autoReleaseStaleAgents(db2) {
27634
28606
  const result = d.run("UPDATE agents SET session_id = NULL WHERE status = 'active' AND session_id IS NOT NULL AND last_seen_at < ?", [cutoff]);
27635
28607
  return result.changes;
27636
28608
  }
27637
- function getAvailableNamesFromPool(pool, db2) {
28609
+ function getAvailableNamesFromPool(pool2, db2) {
27638
28610
  autoReleaseStaleAgents(db2);
27639
28611
  const cutoff = new Date(Date.now() - getActiveWindowMs()).toISOString();
27640
28612
  const activeNames = new Set(db2.query("SELECT name FROM agents WHERE status = 'active' AND last_seen_at > ?").all(cutoff).map((r) => r.name.toLowerCase()));
27641
- return pool.filter((name) => !activeNames.has(name.toLowerCase()));
28613
+ return pool2.filter((name) => !activeNames.has(name.toLowerCase()));
27642
28614
  }
27643
28615
  function shortUuid2() {
27644
28616
  return crypto.randomUUID().slice(0, 8);
@@ -27715,9 +28687,9 @@ function registerAgent5(input, db2) {
27715
28687
  function isAgentConflict2(result) {
27716
28688
  return result.conflict === true;
27717
28689
  }
27718
- function buildConflictError(existing, lastSeenMs, pool, d) {
28690
+ function buildConflictError(existing, lastSeenMs, pool2, d) {
27719
28691
  const minutesAgo = Math.round((Date.now() - lastSeenMs) / 60000);
27720
- const suggestions = pool ? getAvailableNamesFromPool(pool, d) : [];
28692
+ const suggestions = pool2 ? getAvailableNamesFromPool(pool2, d) : [];
27721
28693
  return {
27722
28694
  conflict: true,
27723
28695
  existing_id: existing.id,
@@ -28003,7 +28975,7 @@ function updateSessionActivity(id, db2) {
28003
28975
  const d = db2 || getDatabase3();
28004
28976
  d.run("UPDATE sessions SET last_activity = ? WHERE id = ?", [now2(), id]);
28005
28977
  }
28006
- function deleteSession(id, db2) {
28978
+ function deleteSession2(id, db2) {
28007
28979
  const d = db2 || getDatabase3();
28008
28980
  const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
28009
28981
  return result.changes > 0;
@@ -28822,13 +29794,13 @@ function searchTasks(options, projectId, taskListId, db2) {
28822
29794
  return rows.map(rowToTask3);
28823
29795
  }
28824
29796
  function getTaskListDir(taskListId) {
28825
- return join43(HOME, ".claude", "tasks", taskListId);
29797
+ return join44(HOME, ".claude", "tasks", taskListId);
28826
29798
  }
28827
29799
  function readClaudeTask(dir, filename) {
28828
- return readJsonFile(join43(dir, filename));
29800
+ return readJsonFile(join44(dir, filename));
28829
29801
  }
28830
29802
  function writeClaudeTask(dir, task) {
28831
- writeJsonFile(join43(dir, `${task.id}.json`), task);
29803
+ writeJsonFile(join44(dir, `${task.id}.json`), task);
28832
29804
  }
28833
29805
  function toClaudeStatus(status) {
28834
29806
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -28840,14 +29812,14 @@ function toSqliteStatus(status) {
28840
29812
  return status;
28841
29813
  }
28842
29814
  function readPrefixCounter(dir) {
28843
- const path = join43(dir, ".prefix-counter");
28844
- if (!existsSync42(path))
29815
+ const path = join44(dir, ".prefix-counter");
29816
+ if (!existsSync43(path))
28845
29817
  return 0;
28846
29818
  const val = parseInt(readFileSync24(path, "utf-8").trim(), 10);
28847
29819
  return isNaN(val) ? 0 : val;
28848
29820
  }
28849
29821
  function writePrefixCounter(dir, value) {
28850
- writeFileSync23(join43(dir, ".prefix-counter"), String(value));
29822
+ writeFileSync23(join44(dir, ".prefix-counter"), String(value));
28851
29823
  }
28852
29824
  function formatPrefixedSubject(title, prefix, counter) {
28853
29825
  const padded = String(counter).padStart(5, "0");
@@ -28874,8 +29846,8 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
28874
29846
  }
28875
29847
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
28876
29848
  const dir = getTaskListDir(taskListId);
28877
- if (!existsSync42(dir))
28878
- ensureDir22(dir);
29849
+ if (!existsSync43(dir))
29850
+ ensureDir23(dir);
28879
29851
  const filter = {};
28880
29852
  if (projectId)
28881
29853
  filter["project_id"] = projectId;
@@ -28883,7 +29855,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
28883
29855
  const existingByTodosId = new Map;
28884
29856
  const files = listJsonFiles(dir);
28885
29857
  for (const f of files) {
28886
- const path = join43(dir, f);
29858
+ const path = join44(dir, f);
28887
29859
  const ct = readClaudeTask(dir, f);
28888
29860
  if (ct?.metadata?.["todos_id"]) {
28889
29861
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -28970,7 +29942,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
28970
29942
  }
28971
29943
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
28972
29944
  const dir = getTaskListDir(taskListId);
28973
- if (!existsSync42(dir)) {
29945
+ if (!existsSync43(dir)) {
28974
29946
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
28975
29947
  }
28976
29948
  const files = readdirSync22(dir).filter((f) => f.endsWith(".json"));
@@ -28990,7 +29962,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
28990
29962
  }
28991
29963
  for (const f of files) {
28992
29964
  try {
28993
- const filePath = join43(dir, f);
29965
+ const filePath = join44(dir, f);
28994
29966
  const ct = readClaudeTask(dir, f);
28995
29967
  if (!ct)
28996
29968
  continue;
@@ -29093,7 +30065,7 @@ function metadataKey(agent) {
29093
30065
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
29094
30066
  const dir = getTaskListDir2(agent, taskListId);
29095
30067
  if (!existsSync52(dir))
29096
- ensureDir22(dir);
30068
+ ensureDir23(dir);
29097
30069
  const filter = {};
29098
30070
  if (projectId)
29099
30071
  filter["project_id"] = projectId;
@@ -29401,7 +30373,7 @@ function extractTodos(options, db2) {
29401
30373
  for (const file of files) {
29402
30374
  const fullPath = statSync22(basePath).isFile() ? basePath : join62(basePath, file);
29403
30375
  try {
29404
- const source = readFileSync32(fullPath, "utf-8");
30376
+ const source = readFileSync33(fullPath, "utf-8");
29405
30377
  const relPath = statSync22(basePath).isFile() ? relative(resolve22(basePath, ".."), fullPath) : file;
29406
30378
  const comments = extractFromSource(source, relPath, tags);
29407
30379
  allComments.push(...comments);
@@ -30621,13 +31593,13 @@ __export(exports_dist4, {
30621
31593
  CATEGORIES: () => CATEGORIES,
30622
31594
  AGENT_TARGETS: () => AGENT_TARGETS
30623
31595
  });
30624
- 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";
30625
- import { join as join12, dirname as dirname6 } from "path";
30626
- 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";
30627
31599
  import { fileURLToPath } from "url";
30628
31600
  import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
30629
31601
  import { join as join25 } from "path";
30630
- import { existsSync as existsSync34, readFileSync as readFileSync33, writeFileSync as writeFileSync24, mkdirSync as mkdirSync25 } from "fs";
31602
+ import { existsSync as existsSync34, readFileSync as readFileSync34, writeFileSync as writeFileSync24, mkdirSync as mkdirSync25 } from "fs";
30631
31603
  import { join as join35, dirname as dirname23 } from "path";
30632
31604
  import { homedir as homedir24 } from "os";
30633
31605
  function getSkillsByCategory(category) {
@@ -30734,35 +31706,35 @@ function normalizeSkillName(name) {
30734
31706
  function findSkillsDir() {
30735
31707
  let dir = __dirname2;
30736
31708
  for (let i = 0;i < 5; i++) {
30737
- const candidate = join12(dir, "skills");
30738
- if (existsSync7(candidate)) {
31709
+ const candidate = join13(dir, "skills");
31710
+ if (existsSync8(candidate)) {
30739
31711
  return candidate;
30740
31712
  }
30741
31713
  dir = dirname6(dir);
30742
31714
  }
30743
- return join12(__dirname2, "..", "skills");
31715
+ return join13(__dirname2, "..", "skills");
30744
31716
  }
30745
31717
  function getSkillPath(name) {
30746
31718
  const skillName = normalizeSkillName(name);
30747
- return join12(SKILLS_DIR, skillName);
31719
+ return join13(SKILLS_DIR, skillName);
30748
31720
  }
30749
31721
  function skillExists(name) {
30750
- return existsSync7(getSkillPath(name));
31722
+ return existsSync8(getSkillPath(name));
30751
31723
  }
30752
31724
  function installSkill(name, options = {}) {
30753
31725
  const { targetDir = process.cwd(), overwrite = false } = options;
30754
31726
  const skillName = normalizeSkillName(name);
30755
31727
  const sourcePath = getSkillPath(name);
30756
- const destDir = join12(targetDir, ".skills");
30757
- const destPath = join12(destDir, skillName);
30758
- if (!existsSync7(sourcePath)) {
31728
+ const destDir = join13(targetDir, ".skills");
31729
+ const destPath = join13(destDir, skillName);
31730
+ if (!existsSync8(sourcePath)) {
30759
31731
  return {
30760
31732
  skill: name,
30761
31733
  success: false,
30762
31734
  error: `Skill '${name}' not found`
30763
31735
  };
30764
31736
  }
30765
- if (existsSync7(destPath) && !overwrite) {
31737
+ if (existsSync8(destPath) && !overwrite) {
30766
31738
  return {
30767
31739
  skill: name,
30768
31740
  success: false,
@@ -30771,10 +31743,10 @@ function installSkill(name, options = {}) {
30771
31743
  };
30772
31744
  }
30773
31745
  try {
30774
- if (!existsSync7(destDir)) {
30775
- mkdirSync11(destDir, { recursive: true });
31746
+ if (!existsSync8(destDir)) {
31747
+ mkdirSync12(destDir, { recursive: true });
30776
31748
  }
30777
- if (existsSync7(destPath) && overwrite) {
31749
+ if (existsSync8(destPath) && overwrite) {
30778
31750
  rmSync2(destPath, { recursive: true, force: true });
30779
31751
  }
30780
31752
  cpSync(sourcePath, destPath, {
@@ -30813,10 +31785,10 @@ function installSkills(names, options = {}) {
30813
31785
  return names.map((name) => installSkill(name, options));
30814
31786
  }
30815
31787
  function updateSkillsIndex(skillsDir) {
30816
- const indexPath = join12(skillsDir, "index.ts");
31788
+ const indexPath = join13(skillsDir, "index.ts");
30817
31789
  const meta = loadMeta(skillsDir);
30818
31790
  const disabledSet = new Set(meta.disabled || []);
30819
- 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-", "")));
30820
31792
  const exports = skills.map((s) => {
30821
31793
  const name = s.replace("skill-", "").replace(/-/g, "_");
30822
31794
  return `export * as ${name} from './${s}/src/index.js';`;
@@ -30832,11 +31804,11 @@ ${exports}
30832
31804
  writeFileSync6(indexPath, content);
30833
31805
  }
30834
31806
  function getMetaPath(skillsDir) {
30835
- return join12(skillsDir, ".meta.json");
31807
+ return join13(skillsDir, ".meta.json");
30836
31808
  }
30837
31809
  function loadMeta(skillsDir) {
30838
31810
  const metaPath2 = getMetaPath(skillsDir);
30839
- if (existsSync7(metaPath2)) {
31811
+ if (existsSync8(metaPath2)) {
30840
31812
  try {
30841
31813
  return JSON.parse(readFileSync7(metaPath2, "utf-8"));
30842
31814
  } catch {}
@@ -30851,8 +31823,8 @@ function recordInstall(skillsDir, name) {
30851
31823
  const skillName = normalizeSkillName(name);
30852
31824
  let version = "unknown";
30853
31825
  try {
30854
- const pkgPath = join12(skillsDir, skillName, "package.json");
30855
- if (existsSync7(pkgPath)) {
31826
+ const pkgPath = join13(skillsDir, skillName, "package.json");
31827
+ if (existsSync8(pkgPath)) {
30856
31828
  const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
30857
31829
  version = pkg.version || "unknown";
30858
31830
  }
@@ -30866,12 +31838,12 @@ function recordRemove(skillsDir, name) {
30866
31838
  saveMeta(skillsDir, meta);
30867
31839
  }
30868
31840
  function getInstallMeta(targetDir = process.cwd()) {
30869
- return loadMeta(join12(targetDir, ".skills"));
31841
+ return loadMeta(join13(targetDir, ".skills"));
30870
31842
  }
30871
31843
  function disableSkill(name, targetDir = process.cwd()) {
30872
- const skillsDir = join12(targetDir, ".skills");
31844
+ const skillsDir = join13(targetDir, ".skills");
30873
31845
  const skillName = normalizeSkillName(name);
30874
- if (!existsSync7(join12(skillsDir, skillName)))
31846
+ if (!existsSync8(join13(skillsDir, skillName)))
30875
31847
  return false;
30876
31848
  const meta = loadMeta(skillsDir);
30877
31849
  const disabled = new Set(meta.disabled || []);
@@ -30884,7 +31856,7 @@ function disableSkill(name, targetDir = process.cwd()) {
30884
31856
  return true;
30885
31857
  }
30886
31858
  function enableSkill(name, targetDir = process.cwd()) {
30887
- const skillsDir = join12(targetDir, ".skills");
31859
+ const skillsDir = join13(targetDir, ".skills");
30888
31860
  const meta = loadMeta(skillsDir);
30889
31861
  const disabled = new Set(meta.disabled || []);
30890
31862
  if (!disabled.has(name))
@@ -30896,24 +31868,24 @@ function enableSkill(name, targetDir = process.cwd()) {
30896
31868
  return true;
30897
31869
  }
30898
31870
  function getDisabledSkills(targetDir = process.cwd()) {
30899
- const meta = loadMeta(join12(targetDir, ".skills"));
31871
+ const meta = loadMeta(join13(targetDir, ".skills"));
30900
31872
  return meta.disabled || [];
30901
31873
  }
30902
31874
  function getInstalledSkills(targetDir = process.cwd()) {
30903
- const skillsDir = join12(targetDir, ".skills");
30904
- if (!existsSync7(skillsDir)) {
31875
+ const skillsDir = join13(targetDir, ".skills");
31876
+ if (!existsSync8(skillsDir)) {
30905
31877
  return [];
30906
31878
  }
30907
- return readdirSync5(skillsDir).filter((f) => {
30908
- const fullPath = join12(skillsDir, f);
31879
+ return readdirSync6(skillsDir).filter((f) => {
31880
+ const fullPath = join13(skillsDir, f);
30909
31881
  return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
30910
31882
  }).map((f) => f.replace("skill-", ""));
30911
31883
  }
30912
31884
  function removeSkill(name, targetDir = process.cwd()) {
30913
31885
  const skillName = normalizeSkillName(name);
30914
- const skillsDir = join12(targetDir, ".skills");
30915
- const skillPath = join12(skillsDir, skillName);
30916
- if (!existsSync7(skillPath)) {
31886
+ const skillsDir = join13(targetDir, ".skills");
31887
+ const skillPath = join13(skillsDir, skillName);
31888
+ if (!existsSync8(skillPath)) {
30917
31889
  return false;
30918
31890
  }
30919
31891
  rmSync2(skillPath, { recursive: true, force: true });
@@ -30924,24 +31896,24 @@ function removeSkill(name, targetDir = process.cwd()) {
30924
31896
  function getAgentSkillsDir(agent, scope = "global", projectDir) {
30925
31897
  const agentDir = `.${agent}`;
30926
31898
  if (scope === "project") {
30927
- return join12(projectDir || process.cwd(), agentDir, "skills");
31899
+ return join13(projectDir || process.cwd(), agentDir, "skills");
30928
31900
  }
30929
- return join12(homedir11(), agentDir, "skills");
31901
+ return join13(homedir12(), agentDir, "skills");
30930
31902
  }
30931
31903
  function getAgentSkillPath(name, agent, scope = "global", projectDir) {
30932
31904
  const skillName = normalizeSkillName(name);
30933
- return join12(getAgentSkillsDir(agent, scope, projectDir), skillName);
31905
+ return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
30934
31906
  }
30935
31907
  function installSkillForAgent(name, options, generateSkillMd) {
30936
31908
  const { agent, scope = "global", projectDir } = options;
30937
31909
  const skillName = normalizeSkillName(name);
30938
31910
  const sourcePath = getSkillPath(name);
30939
- if (!existsSync7(sourcePath)) {
31911
+ if (!existsSync8(sourcePath)) {
30940
31912
  return { skill: name, success: false, error: `Skill '${name}' not found` };
30941
31913
  }
30942
31914
  let skillMdContent = null;
30943
- const skillMdPath = join12(sourcePath, "SKILL.md");
30944
- if (existsSync7(skillMdPath)) {
31915
+ const skillMdPath = join13(sourcePath, "SKILL.md");
31916
+ if (existsSync8(skillMdPath)) {
30945
31917
  skillMdContent = readFileSync7(skillMdPath, "utf-8");
30946
31918
  } else if (generateSkillMd) {
30947
31919
  skillMdContent = generateSkillMd(name);
@@ -30951,8 +31923,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
30951
31923
  }
30952
31924
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
30953
31925
  if (scope === "global") {
30954
- const agentBaseDir2 = join12(homedir11(), `.${agent}`);
30955
- if (!existsSync7(agentBaseDir2)) {
31926
+ const agentBaseDir2 = join13(homedir12(), `.${agent}`);
31927
+ if (!existsSync8(agentBaseDir2)) {
30956
31928
  const agentLabels = {
30957
31929
  claude: "Claude Code",
30958
31930
  codex: "Codex CLI",
@@ -30975,8 +31947,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
30975
31947
  }
30976
31948
  }
30977
31949
  try {
30978
- mkdirSync11(destDir, { recursive: true });
30979
- writeFileSync6(join12(destDir, "SKILL.md"), skillMdContent);
31950
+ mkdirSync12(destDir, { recursive: true });
31951
+ writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
30980
31952
  return { skill: name, success: true, path: destDir };
30981
31953
  } catch (error) {
30982
31954
  return {
@@ -30989,7 +31961,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
30989
31961
  function removeSkillForAgent(name, options) {
30990
31962
  const { agent, scope = "global", projectDir } = options;
30991
31963
  const destDir = getAgentSkillPath(name, agent, scope, projectDir);
30992
- if (!existsSync7(destDir)) {
31964
+ if (!existsSync8(destDir)) {
30993
31965
  return false;
30994
31966
  }
30995
31967
  rmSync2(destDir, { recursive: true, force: true });
@@ -31281,7 +32253,7 @@ function readConfigFile(path) {
31281
32253
  if (!existsSync34(path))
31282
32254
  return {};
31283
32255
  try {
31284
- const raw = readFileSync33(path, "utf-8");
32256
+ const raw = readFileSync34(path, "utf-8");
31285
32257
  const parsed = JSON.parse(raw);
31286
32258
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
31287
32259
  return {};
@@ -31314,7 +32286,7 @@ function saveConfig(key, value, scope = "project") {
31314
32286
  let existing = {};
31315
32287
  if (existsSync34(filePath)) {
31316
32288
  try {
31317
- existing = JSON.parse(readFileSync33(filePath, "utf-8"));
32289
+ existing = JSON.parse(readFileSync34(filePath, "utf-8"));
31318
32290
  if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
31319
32291
  existing = {};
31320
32292
  }
@@ -32913,7 +33885,7 @@ __export(exports_cron_manager, {
32913
33885
  deleteCronJob: () => deleteCronJob,
32914
33886
  createCronJob: () => createCronJob
32915
33887
  });
32916
- import { randomUUID as randomUUID11 } from "crypto";
33888
+ import { randomUUID as randomUUID12 } from "crypto";
32917
33889
  function ensureCronTable() {
32918
33890
  const db2 = getDatabase();
32919
33891
  db2.exec(`
@@ -32942,7 +33914,7 @@ function ensureCronTable() {
32942
33914
  function createCronJob(schedule, task, name) {
32943
33915
  ensureCronTable();
32944
33916
  const db2 = getDatabase();
32945
- const id = randomUUID11();
33917
+ const id = randomUUID12();
32946
33918
  db2.prepare(`
32947
33919
  INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
32948
33920
  VALUES (?, ?, ?, ?, 1)
@@ -33000,7 +33972,7 @@ function getCronEvents(jobId, limit = 10) {
33000
33972
  async function executeCronJob(job) {
33001
33973
  ensureCronTable();
33002
33974
  const db2 = getDatabase();
33003
- const eventId = randomUUID11();
33975
+ const eventId = randomUUID12();
33004
33976
  const startedAt = new Date().toISOString();
33005
33977
  db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
33006
33978
  try {
@@ -33091,7 +34063,7 @@ __export(exports_url_watcher, {
33091
34063
  deleteWatchJob: () => deleteWatchJob,
33092
34064
  createWatchJob: () => createWatchJob
33093
34065
  });
33094
- import { randomUUID as randomUUID12 } from "crypto";
34066
+ import { randomUUID as randomUUID13 } from "crypto";
33095
34067
  import { createHash } from "crypto";
33096
34068
  function ensureWatchTables() {
33097
34069
  const db2 = getDatabase();
@@ -33123,7 +34095,7 @@ function ensureWatchTables() {
33123
34095
  function createWatchJob(url, schedule, opts) {
33124
34096
  ensureWatchTables();
33125
34097
  const db2 = getDatabase();
33126
- const id = randomUUID12();
34098
+ const id = randomUUID13();
33127
34099
  db2.prepare(`
33128
34100
  INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
33129
34101
  VALUES (?, ?, ?, ?, ?, ?, 1)
@@ -33159,7 +34131,7 @@ function getWatchEvents(watchId, limit = 20) {
33159
34131
  async function checkWatchJob(job) {
33160
34132
  ensureWatchTables();
33161
34133
  const db2 = getDatabase();
33162
- const eventId = randomUUID12();
34134
+ const eventId = randomUUID13();
33163
34135
  const checkedAt = new Date().toISOString();
33164
34136
  let newContent = "";
33165
34137
  try {
@@ -33256,7 +34228,7 @@ ${snap.tree.slice(0, 2000)}`;
33256
34228
  const response = await client.messages.create({
33257
34229
  model,
33258
34230
  max_tokens: 512,
33259
- system: SYSTEM_PROMPT,
34231
+ system: SYSTEM_PROMPT2,
33260
34232
  messages: [{
33261
34233
  role: "user",
33262
34234
  content: `Task: ${task}
@@ -33321,7 +34293,7 @@ What actions should I take next? Return JSON array.`
33321
34293
  }
33322
34294
  return { success: false, result: null, steps_taken: steps.length, steps, cost_estimate: totalTokens / 1000 * 0.00025, error: `Reached max steps (${maxSteps}) without completing task` };
33323
34295
  }
33324
- var SYSTEM_PROMPT = `You are a browser automation agent. Given a task and the current page state, decide which browser actions to take.
34296
+ var SYSTEM_PROMPT2 = `You are a browser automation agent. Given a task and the current page state, decide which browser actions to take.
33325
34297
 
33326
34298
  Return a JSON array of at most 3 actions to execute next:
33327
34299
  [{"tool": "navigate|click|type|scroll|evaluate|done", "args": {...}, "reason": "..."}]
@@ -33335,7 +34307,7 @@ var exports_mcp = {};
33335
34307
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33336
34308
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33337
34309
  import { readFileSync as readFileSync8 } from "fs";
33338
- import { join as join13 } from "path";
34310
+ import { join as join14 } from "path";
33339
34311
  function json(data) {
33340
34312
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
33341
34313
  }
@@ -33347,6 +34319,34 @@ function err(e) {
33347
34319
  isError: true
33348
34320
  };
33349
34321
  }
34322
+ async function errWithScreenshot(e, sessionId) {
34323
+ const msg = e instanceof Error ? e.message : String(e);
34324
+ const code = e instanceof BrowserError ? e.code : "ERROR";
34325
+ let screenshot_path;
34326
+ if (sessionId) {
34327
+ try {
34328
+ const sid = resolveSessionId(sessionId);
34329
+ const page = getSessionPage(sid);
34330
+ const result = await takeScreenshot(page, { maxWidth: 800, quality: 50, track: false, thumbnail: false });
34331
+ screenshot_path = result.path;
34332
+ } catch {}
34333
+ }
34334
+ return {
34335
+ content: [{ type: "text", text: JSON.stringify({ error: msg, code, error_screenshot: screenshot_path }) }],
34336
+ isError: true
34337
+ };
34338
+ }
34339
+ function resolveSessionId(sessionId) {
34340
+ if (sessionId)
34341
+ return sessionId;
34342
+ const def = getDefaultSession();
34343
+ if (def)
34344
+ return def.session.id;
34345
+ const count = countActiveSessions2();
34346
+ if (count === 0)
34347
+ throw new BrowserError("No active sessions. Create one with browser_session_create first.", "NO_SESSION");
34348
+ throw new BrowserError(`${count} active sessions \u2014 specify session_id to choose one.`, "AMBIGUOUS_SESSION");
34349
+ }
33350
34350
  var _pkg, networkLogCleanup, consoleCaptureCleanup, harCaptures, server, activeWatchHandles2, _startupToolCount, transport;
33351
34351
  var init_mcp = __esm(async () => {
33352
34352
  init_zod();
@@ -33368,10 +34368,11 @@ var init_mcp = __esm(async () => {
33368
34368
  init_snapshot();
33369
34369
  init_files_integration();
33370
34370
  init_recordings();
34371
+ init_timeline();
33371
34372
  init_dialogs();
33372
34373
  init_profiles();
33373
34374
  init_types();
33374
- _pkg = JSON.parse(readFileSync8(join13(import.meta.dir, "../../package.json"), "utf8"));
34375
+ _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
33375
34376
  networkLogCleanup = new Map;
33376
34377
  consoleCaptureCleanup = new Map;
33377
34378
  harCaptures = new Map;
@@ -33379,7 +34380,7 @@ var init_mcp = __esm(async () => {
33379
34380
  name: "@hasna/browser",
33380
34381
  version: "0.0.1"
33381
34382
  });
33382
- server.tool("browser_session_create", "Create a new browser session with the specified engine", {
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.", {
33383
34384
  engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
33384
34385
  use_case: exports_external.string().optional(),
33385
34386
  project_id: exports_external.string().optional(),
@@ -33388,9 +34389,19 @@ var init_mcp = __esm(async () => {
33388
34389
  headless: exports_external.boolean().optional().default(true),
33389
34390
  viewport_width: exports_external.number().optional().default(1280),
33390
34391
  viewport_height: exports_external.number().optional().default(720),
33391
- stealth: exports_external.boolean().optional().default(false)
33392
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
34392
+ stealth: exports_external.boolean().optional().default(false),
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)"),
34395
+ force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
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 }) => {
33393
34399
  try {
34400
+ if (agent_id && !force_new) {
34401
+ const existing = getActiveSessionForAgent2(agent_id);
34402
+ if (existing)
34403
+ return json({ session: existing.session, reused: true });
34404
+ }
33394
34405
  const { session } = await createSession2({
33395
34406
  engine,
33396
34407
  useCase: use_case,
@@ -33399,44 +34410,68 @@ var init_mcp = __esm(async () => {
33399
34410
  startUrl: start_url,
33400
34411
  headless,
33401
34412
  viewport: { width: viewport_width, height: viewport_height },
33402
- stealth
34413
+ stealth,
34414
+ autoGallery: auto_gallery,
34415
+ storageState: storage_state,
34416
+ cdpUrl: cdp_url
33403
34417
  });
33404
- return json({ session });
34418
+ if (tags?.length) {
34419
+ const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
34420
+ for (const tag of tags)
34421
+ addSessionTag2(session.id, tag);
34422
+ }
34423
+ logEvent(session.id, "session_created", { engine: session.engine });
34424
+ return json({ session, reused: false });
33405
34425
  } catch (e) {
33406
34426
  return err(e);
33407
34427
  }
33408
34428
  });
33409
- server.tool("browser_session_list", "List all browser sessions", { status: exports_external.enum(["active", "closed", "error"]).optional(), project_id: exports_external.string().optional() }, async ({ status, project_id }) => {
34429
+ server.tool("browser_session_list", "List all browser sessions. Optionally filter by tag.", { status: exports_external.enum(["active", "closed", "error"]).optional(), project_id: exports_external.string().optional(), tag: exports_external.string().optional() }, async ({ status, project_id, tag }) => {
33410
34430
  try {
34431
+ if (tag) {
34432
+ const { listSessionsByTag: listSessionsByTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
34433
+ return json({ sessions: listSessionsByTag2(tag) });
34434
+ }
33411
34435
  return json({ sessions: listSessions2({ status, projectId: project_id }) });
33412
34436
  } catch (e) {
33413
34437
  return err(e);
33414
34438
  }
33415
34439
  });
33416
- server.tool("browser_session_close", "Close a browser session", { session_id: exports_external.string() }, async ({ session_id }) => {
34440
+ server.tool("browser_session_close", "Close a browser session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33417
34441
  try {
33418
- const session = await closeSession2(session_id);
33419
- networkLogCleanup.get(session_id)?.();
33420
- consoleCaptureCleanup.get(session_id)?.();
33421
- networkLogCleanup.delete(session_id);
33422
- consoleCaptureCleanup.delete(session_id);
33423
- harCaptures.delete(session_id);
34442
+ const sid = resolveSessionId(session_id);
34443
+ const session = await closeSession2(sid);
34444
+ networkLogCleanup.get(sid)?.();
34445
+ consoleCaptureCleanup.get(sid)?.();
34446
+ networkLogCleanup.delete(sid);
34447
+ consoleCaptureCleanup.delete(sid);
34448
+ harCaptures.delete(sid);
33424
34449
  return json({ session });
33425
34450
  } catch (e) {
33426
34451
  return err(e);
33427
34452
  }
33428
34453
  });
34454
+ server.tool("browser_session_timeline", "Get chronological action log for a session", { session_id: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }, async ({ session_id, limit }) => {
34455
+ try {
34456
+ const sid = resolveSessionId(session_id);
34457
+ const events = getTimeline(sid, limit);
34458
+ return json({ events, count: events.length });
34459
+ } catch (e) {
34460
+ return err(e);
34461
+ }
34462
+ });
33429
34463
  server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
33430
- session_id: exports_external.string(),
34464
+ session_id: exports_external.string().optional(),
33431
34465
  url: exports_external.string(),
33432
34466
  timeout: exports_external.number().optional().default(30000),
33433
34467
  auto_snapshot: exports_external.boolean().optional().default(true),
33434
34468
  auto_thumbnail: exports_external.boolean().optional().default(true)
33435
34469
  }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
33436
34470
  try {
33437
- const page = getSessionPage(session_id);
33438
- if (isBunSession(session_id)) {
33439
- const bunView = getSessionBunView(session_id);
34471
+ const sid = resolveSessionId(session_id);
34472
+ const page = getSessionPage(sid);
34473
+ if (isBunSession(sid)) {
34474
+ const bunView = getSessionBunView(sid);
33440
34475
  await bunView.goto(url, { timeout });
33441
34476
  await new Promise((r) => setTimeout(r, 500));
33442
34477
  } else {
@@ -33463,10 +34498,10 @@ var init_mcp = __esm(async () => {
33463
34498
  } catch {}
33464
34499
  }
33465
34500
  try {
33466
- const session = getSession2(session_id);
34501
+ const session = getSession2(sid);
33467
34502
  if (!session.name) {
33468
34503
  const hostname = new URL(current_url).hostname;
33469
- renameSession2(session_id, hostname);
34504
+ renameSession2(sid, hostname);
33470
34505
  }
33471
34506
  } catch {}
33472
34507
  const result = {
@@ -33482,86 +34517,110 @@ var init_mcp = __esm(async () => {
33482
34517
  result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
33483
34518
  } catch {}
33484
34519
  }
33485
- if (isBunSession(session_id) && auto_snapshot) {
34520
+ if (isAutoGallery(sid)) {
34521
+ try {
34522
+ const ss = await takeScreenshot(page, { maxWidth: 1280, quality: 70, thumbnail: true });
34523
+ const { createEntry: createEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
34524
+ createEntry2({ session_id: sid, url: current_url, title, path: ss.path, thumbnail_path: ss.thumbnail_path, format: "webp", width: ss.width, height: ss.height, original_size_bytes: ss.original_size_bytes, compressed_size_bytes: ss.compressed_size_bytes, compression_ratio: ss.compression_ratio, tags: [], is_favorite: false });
34525
+ } catch {}
34526
+ }
34527
+ if (isBunSession(sid) && auto_snapshot) {
33486
34528
  await new Promise((r) => setTimeout(r, 200));
33487
34529
  }
33488
34530
  if (auto_snapshot) {
33489
34531
  try {
33490
- const snap = await takeSnapshot(page, session_id);
33491
- setLastSnapshot(session_id, snap);
34532
+ const snap = await takeSnapshot(page, sid);
34533
+ setLastSnapshot(sid, snap);
33492
34534
  const refEntries = Object.entries(snap.refs).slice(0, 30);
33493
34535
  result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
33494
34536
  result.interactive_count = snap.interactive_count;
33495
- result.has_errors = getConsoleLog(session_id, "error").length > 0;
34537
+ result.has_errors = getConsoleLog(sid, "error").length > 0;
33496
34538
  } catch {}
33497
34539
  }
34540
+ logEvent(sid, "navigate", { url, title, current_url });
33498
34541
  return json(result);
33499
34542
  } catch (e) {
33500
- return err(e);
34543
+ return errWithScreenshot(e, session_id);
33501
34544
  }
33502
34545
  });
33503
- server.tool("browser_back", "Navigate back in browser history", { session_id: exports_external.string() }, async ({ session_id }) => {
34546
+ server.tool("browser_back", "Navigate back in browser history", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33504
34547
  try {
33505
- const page = getSessionPage(session_id);
34548
+ const sid = resolveSessionId(session_id);
34549
+ const page = getSessionPage(sid);
33506
34550
  await goBack(page);
33507
34551
  return json({ url: page.url() });
33508
34552
  } catch (e) {
33509
34553
  return err(e);
33510
34554
  }
33511
34555
  });
33512
- server.tool("browser_forward", "Navigate forward in browser history", { session_id: exports_external.string() }, async ({ session_id }) => {
34556
+ server.tool("browser_forward", "Navigate forward in browser history", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33513
34557
  try {
33514
- const page = getSessionPage(session_id);
34558
+ const sid = resolveSessionId(session_id);
34559
+ const page = getSessionPage(sid);
33515
34560
  await goForward(page);
33516
34561
  return json({ url: page.url() });
33517
34562
  } catch (e) {
33518
34563
  return err(e);
33519
34564
  }
33520
34565
  });
33521
- server.tool("browser_reload", "Reload the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
34566
+ server.tool("browser_reload", "Reload the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33522
34567
  try {
33523
- const page = getSessionPage(session_id);
34568
+ const sid = resolveSessionId(session_id);
34569
+ const page = getSessionPage(sid);
33524
34570
  await reload(page);
33525
34571
  return json({ url: page.url() });
33526
34572
  } catch (e) {
33527
34573
  return err(e);
33528
34574
  }
33529
34575
  });
33530
- server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
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 }) => {
33531
34577
  try {
33532
- const page = getSessionPage(session_id);
34578
+ const sid = resolveSessionId(session_id);
34579
+ const page = getSessionPage(sid);
33533
34580
  if (ref) {
33534
- await clickRef(page, session_id, ref, { timeout });
34581
+ await clickRef(page, sid, ref, { timeout });
34582
+ logEvent(sid, "click", { selector: ref, method: "ref" });
33535
34583
  return json({ clicked: ref, method: "ref" });
33536
34584
  }
33537
34585
  if (!selector)
33538
34586
  return err(new Error("Either ref or selector is required"));
33539
- await click(page, selector, { button, timeout });
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
+ }
33540
34592
  return json({ clicked: selector, method: "selector" });
33541
34593
  } catch (e) {
33542
- return err(e);
34594
+ return errWithScreenshot(e, session_id);
33543
34595
  }
33544
34596
  });
33545
- server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
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 }) => {
33546
34598
  try {
33547
- const page = getSessionPage(session_id);
34599
+ const sid = resolveSessionId(session_id);
34600
+ const page = getSessionPage(sid);
33548
34601
  if (ref) {
33549
- await typeRef(page, session_id, ref, text, { clear, delay });
34602
+ await typeRef(page, sid, ref, text, { clear, delay });
34603
+ logEvent(sid, "type", { selector: ref, text: text.slice(0, 100) });
33550
34604
  return json({ typed: text, ref, method: "ref" });
33551
34605
  }
33552
34606
  if (!selector)
33553
34607
  return err(new Error("Either ref or selector is required"));
33554
- await type(page, selector, text, { clear, delay });
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
+ }
33555
34613
  return json({ typed: text, selector, method: "selector" });
33556
34614
  } catch (e) {
33557
- return err(e);
34615
+ return errWithScreenshot(e, session_id);
33558
34616
  }
33559
34617
  });
33560
- server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
34618
+ server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
33561
34619
  try {
33562
- const page = getSessionPage(session_id);
34620
+ const sid = resolveSessionId(session_id);
34621
+ const page = getSessionPage(sid);
33563
34622
  if (ref) {
33564
- await hoverRef(page, session_id, ref);
34623
+ await hoverRef(page, sid, ref);
33565
34624
  return json({ hovered: ref, method: "ref" });
33566
34625
  }
33567
34626
  if (!selector)
@@ -33572,20 +34631,22 @@ var init_mcp = __esm(async () => {
33572
34631
  return err(e);
33573
34632
  }
33574
34633
  });
33575
- server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount }) => {
34634
+ server.tool("browser_scroll", "Scroll the page", { session_id: exports_external.string().optional(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount }) => {
33576
34635
  try {
33577
- const page = getSessionPage(session_id);
34636
+ const sid = resolveSessionId(session_id);
34637
+ const page = getSessionPage(sid);
33578
34638
  await scroll(page, direction, amount);
33579
34639
  return json({ scrolled: direction, amount });
33580
34640
  } catch (e) {
33581
34641
  return err(e);
33582
34642
  }
33583
34643
  });
33584
- server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
34644
+ server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
33585
34645
  try {
33586
- const page = getSessionPage(session_id);
34646
+ const sid = resolveSessionId(session_id);
34647
+ const page = getSessionPage(sid);
33587
34648
  if (ref) {
33588
- const selected2 = await selectRef(page, session_id, ref, value);
34649
+ const selected2 = await selectRef(page, sid, ref, value);
33589
34650
  return json({ selected: selected2, method: "ref" });
33590
34651
  }
33591
34652
  if (!selector)
@@ -33596,11 +34657,12 @@ var init_mcp = __esm(async () => {
33596
34657
  return err(e);
33597
34658
  }
33598
34659
  });
33599
- server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
34660
+ server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
33600
34661
  try {
33601
- const page = getSessionPage(session_id);
34662
+ const sid = resolveSessionId(session_id);
34663
+ const page = getSessionPage(sid);
33602
34664
  if (ref) {
33603
- await checkRef(page, session_id, ref, checked);
34665
+ await checkRef(page, sid, ref, checked);
33604
34666
  return json({ checked, ref, method: "ref" });
33605
34667
  }
33606
34668
  if (!selector)
@@ -33611,75 +34673,111 @@ var init_mcp = __esm(async () => {
33611
34673
  return err(e);
33612
34674
  }
33613
34675
  });
33614
- server.tool("browser_upload", "Upload a file to an input element", { session_id: exports_external.string(), selector: exports_external.string(), file_path: exports_external.string() }, async ({ session_id, selector, file_path }) => {
34676
+ server.tool("browser_upload", "Upload a file to an input element", { session_id: exports_external.string().optional(), selector: exports_external.string(), file_path: exports_external.string() }, async ({ session_id, selector, file_path }) => {
33615
34677
  try {
33616
- const page = getSessionPage(session_id);
34678
+ const sid = resolveSessionId(session_id);
34679
+ const page = getSessionPage(sid);
33617
34680
  await uploadFile(page, selector, file_path);
33618
34681
  return json({ uploaded: file_path, selector });
33619
34682
  } catch (e) {
33620
34683
  return err(e);
33621
34684
  }
33622
34685
  });
33623
- server.tool("browser_press_key", "Press a keyboard key", { session_id: exports_external.string(), key: exports_external.string() }, async ({ session_id, key }) => {
34686
+ server.tool("browser_press_key", "Press a keyboard key", { session_id: exports_external.string().optional(), key: exports_external.string() }, async ({ session_id, key }) => {
33624
34687
  try {
33625
- const page = getSessionPage(session_id);
34688
+ const sid = resolveSessionId(session_id);
34689
+ const page = getSessionPage(sid);
33626
34690
  await pressKey(page, key);
33627
34691
  return json({ pressed: key });
33628
34692
  } catch (e) {
33629
34693
  return err(e);
33630
34694
  }
33631
34695
  });
33632
- server.tool("browser_wait", "Wait for a selector to appear", { session_id: exports_external.string(), selector: exports_external.string(), state: exports_external.enum(["attached", "detached", "visible", "hidden"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, state, timeout }) => {
34696
+ server.tool("browser_wait", "Wait for a selector to appear", { session_id: exports_external.string().optional(), selector: exports_external.string(), state: exports_external.enum(["attached", "detached", "visible", "hidden"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, state, timeout }) => {
33633
34697
  try {
33634
- const page = getSessionPage(session_id);
34698
+ const sid = resolveSessionId(session_id);
34699
+ const page = getSessionPage(sid);
33635
34700
  await waitForSelector(page, selector, { state, timeout });
33636
34701
  return json({ ready: selector });
33637
34702
  } catch (e) {
33638
34703
  return err(e);
33639
34704
  }
33640
34705
  });
33641
- server.tool("browser_get_text", "Get text content from the page or a selector", { session_id: exports_external.string(), 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 }) => {
33642
34707
  try {
33643
- const page = getSessionPage(session_id);
33644
- return json({ text: await getText(page, selector) });
34708
+ const sid = resolveSessionId(session_id);
34709
+ const page = getSessionPage(sid);
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 });
33645
34717
  } catch (e) {
33646
34718
  return err(e);
33647
34719
  }
33648
34720
  });
33649
- server.tool("browser_get_html", "Get HTML content from the page or a selector", { session_id: exports_external.string(), 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 }) => {
33650
34722
  try {
33651
- const page = getSessionPage(session_id);
33652
- return json({ html: await getHTML(page, selector) });
34723
+ const sid = resolveSessionId(session_id);
34724
+ const page = getSessionPage(sid);
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 });
33653
34732
  } catch (e) {
33654
34733
  return err(e);
33655
34734
  }
33656
34735
  });
33657
- server.tool("browser_get_links", "Get all links from the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
34736
+ server.tool("browser_get_links", "Get all links from the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33658
34737
  try {
33659
- const page = getSessionPage(session_id);
34738
+ const sid = resolveSessionId(session_id);
34739
+ const page = getSessionPage(sid);
33660
34740
  const links = await getLinks(page);
33661
34741
  return json({ links, count: links.length });
33662
34742
  } catch (e) {
33663
34743
  return err(e);
33664
34744
  }
33665
34745
  });
33666
- server.tool("browser_extract", "Extract content from the page in a specified format", {
33667
- session_id: exports_external.string(),
34746
+ server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
34747
+ session_id: exports_external.string().optional(),
33668
34748
  format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
33669
34749
  selector: exports_external.string().optional(),
33670
- schema: exports_external.record(exports_external.string()).optional()
33671
- }, 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 }) => {
33672
34753
  try {
33673
- const page = getSessionPage(session_id);
34754
+ const sid = resolveSessionId(session_id);
34755
+ const page = getSessionPage(sid);
33674
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
+ }
33675
34772
  return json(result);
33676
34773
  } catch (e) {
33677
34774
  return err(e);
33678
34775
  }
33679
34776
  });
33680
- server.tool("browser_find", "Find elements matching a selector and return their text", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
34777
+ server.tool("browser_find", "Find elements matching a selector and return their text", { session_id: exports_external.string().optional(), selector: exports_external.string() }, async ({ session_id, selector }) => {
33681
34778
  try {
33682
- const page = getSessionPage(session_id);
34779
+ const sid = resolveSessionId(session_id);
34780
+ const page = getSessionPage(sid);
33683
34781
  const elements = await findElements(page, selector);
33684
34782
  const texts = await Promise.all(elements.map((el) => el.textContent()));
33685
34783
  return json({ count: elements.length, texts });
@@ -33687,16 +34785,27 @@ var init_mcp = __esm(async () => {
33687
34785
  return err(e);
33688
34786
  }
33689
34787
  });
33690
- 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.", {
33691
- session_id: exports_external.string(),
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.", {
34789
+ session_id: exports_external.string().optional(),
33692
34790
  compact: exports_external.boolean().optional().default(true),
33693
34791
  max_refs: exports_external.number().optional().default(50),
33694
- full_tree: exports_external.boolean().optional().default(false)
33695
- }, 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 }) => {
33696
34795
  try {
33697
- const page = getSessionPage(session_id);
33698
- const result = await takeSnapshot(page, session_id);
33699
- setLastSnapshot(session_id, result);
34796
+ const sid = resolveSessionId(session_id);
34797
+ const page = getSessionPage(sid);
34798
+ const result = await takeSnapshot(page, sid);
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
+ }
33700
34809
  const refEntries = Object.entries(result.refs).slice(0, max_refs);
33701
34810
  const limitedRefs = Object.fromEntries(refEntries);
33702
34811
  const truncated = Object.keys(result.refs).length > max_refs;
@@ -33708,32 +34817,35 @@ var init_mcp = __esm(async () => {
33708
34817
  interactive_count: result.interactive_count,
33709
34818
  shown_count: refEntries.length,
33710
34819
  truncated,
33711
- refs: limitedRefs
34820
+ refs: limitedRefs,
34821
+ ...injection_warnings ? { injection_warnings } : {}
33712
34822
  });
33713
34823
  }
33714
34824
  const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
33715
34825
  ... (truncated \u2014 use full_tree=true for complete)` : "");
33716
- 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 } : {} });
33717
34827
  } catch (e) {
33718
34828
  return err(e);
33719
34829
  }
33720
34830
  });
33721
- server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
33722
- session_id: exports_external.string(),
33723
- selector: exports_external.string().optional(),
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.", {
34832
+ session_id: exports_external.string().optional(),
34833
+ selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
33724
34834
  full_page: exports_external.boolean().optional().default(false),
33725
34835
  format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
33726
- quality: exports_external.number().optional(),
33727
- max_width: exports_external.number().optional().default(1280),
34836
+ quality: exports_external.number().optional().default(60),
34837
+ max_width: exports_external.number().optional().default(800),
33728
34838
  compress: exports_external.boolean().optional().default(true),
33729
34839
  thumbnail: exports_external.boolean().optional().default(true),
33730
- annotate: exports_external.boolean().optional().default(false)
33731
- }, 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 }) => {
33732
34843
  try {
33733
- const page = getSessionPage(session_id);
34844
+ const sid = resolveSessionId(session_id);
34845
+ const page = getSessionPage(sid);
33734
34846
  if (annotate && !selector && !full_page) {
33735
34847
  const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
33736
- const annotated = await annotateScreenshot2(page, session_id);
34848
+ const annotated = await annotateScreenshot2(page, sid);
33737
34849
  const base64 = annotated.buffer.toString("base64");
33738
34850
  return json({
33739
34851
  base64: base64.length > 50000 ? undefined : base64,
@@ -33744,37 +34856,42 @@ var init_mcp = __esm(async () => {
33744
34856
  annotation_count: annotated.annotations.length
33745
34857
  });
33746
34858
  }
33747
- 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 });
33748
34862
  result.url = page.url();
33749
34863
  try {
33750
34864
  const buf = Buffer.from(result.base64, "base64");
33751
34865
  const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
33752
- const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
34866
+ const dl = saveToDownloads(buf, filename, { sessionId: sid, type: "screenshot", sourceUrl: page.url() });
33753
34867
  result.download_id = dl.id;
33754
34868
  } catch {}
33755
- if (result.base64.length > 50000) {
34869
+ result.estimated_tokens = Math.ceil(result.base64.length / 4);
34870
+ if (detail !== "high" && result.base64.length > 40000) {
33756
34871
  result.base64_truncated = true;
33757
34872
  result.full_image_path = result.path;
33758
34873
  result.base64 = result.thumbnail_base64 ?? "";
33759
34874
  }
34875
+ logEvent(sid, "screenshot", { path: result.path, detail, selector });
33760
34876
  return json(result);
33761
34877
  } catch (e) {
33762
34878
  return err(e);
33763
34879
  }
33764
34880
  });
33765
34881
  server.tool("browser_pdf", "Generate a PDF of the current page", {
33766
- session_id: exports_external.string(),
34882
+ session_id: exports_external.string().optional(),
33767
34883
  format: exports_external.enum(["A4", "Letter", "A3", "A5"]).optional().default("A4"),
33768
34884
  landscape: exports_external.boolean().optional().default(false),
33769
34885
  print_background: exports_external.boolean().optional().default(true)
33770
34886
  }, async ({ session_id, format, landscape, print_background }) => {
33771
34887
  try {
33772
- const page = getSessionPage(session_id);
34888
+ const sid = resolveSessionId(session_id);
34889
+ const page = getSessionPage(sid);
33773
34890
  const result = await generatePDF(page, { format, landscape, printBackground: print_background });
33774
34891
  try {
33775
34892
  const buf = Buffer.from(result.base64, "base64");
33776
34893
  const filename = result.path.split("/").pop() ?? "document.pdf";
33777
- const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "pdf", sourceUrl: page.url() });
34894
+ const dl = saveToDownloads(buf, filename, { sessionId: sid, type: "pdf", sourceUrl: page.url() });
33778
34895
  result.download_id = dl.id;
33779
34896
  } catch {}
33780
34897
  return json(result);
@@ -33782,25 +34899,27 @@ var init_mcp = __esm(async () => {
33782
34899
  return err(e);
33783
34900
  }
33784
34901
  });
33785
- server.tool("browser_evaluate", "Execute JavaScript in the page context", { session_id: exports_external.string(), script: exports_external.string() }, async ({ session_id, script }) => {
34902
+ server.tool("browser_evaluate", "Execute JavaScript in the page context", { session_id: exports_external.string().optional(), script: exports_external.string() }, async ({ session_id, script }) => {
33786
34903
  try {
33787
- const page = getSessionPage(session_id);
34904
+ const sid = resolveSessionId(session_id);
34905
+ const page = getSessionPage(sid);
33788
34906
  const result = await page.evaluate(script);
33789
34907
  return json({ result });
33790
34908
  } catch (e) {
33791
- return err(e);
34909
+ return errWithScreenshot(e, session_id);
33792
34910
  }
33793
34911
  });
33794
- server.tool("browser_cookies_get", "Get cookies from the current session", { session_id: exports_external.string(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
34912
+ server.tool("browser_cookies_get", "Get cookies from the current session", { session_id: exports_external.string().optional(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
33795
34913
  try {
33796
- const page = getSessionPage(session_id);
34914
+ const sid = resolveSessionId(session_id);
34915
+ const page = getSessionPage(sid);
33797
34916
  return json({ cookies: await getCookies(page, { name, domain }) });
33798
34917
  } catch (e) {
33799
34918
  return err(e);
33800
34919
  }
33801
34920
  });
33802
34921
  server.tool("browser_cookies_set", "Set a cookie in the current session", {
33803
- session_id: exports_external.string(),
34922
+ session_id: exports_external.string().optional(),
33804
34923
  name: exports_external.string(),
33805
34924
  value: exports_external.string(),
33806
34925
  domain: exports_external.string().optional(),
@@ -33810,7 +34929,8 @@ var init_mcp = __esm(async () => {
33810
34929
  secure: exports_external.boolean().optional().default(false)
33811
34930
  }, async ({ session_id, name, value, domain, path, expires, http_only, secure }) => {
33812
34931
  try {
33813
- const page = getSessionPage(session_id);
34932
+ const sid = resolveSessionId(session_id);
34933
+ const page = getSessionPage(sid);
33814
34934
  await setCookie(page, {
33815
34935
  name,
33816
34936
  value,
@@ -33826,27 +34946,30 @@ var init_mcp = __esm(async () => {
33826
34946
  return err(e);
33827
34947
  }
33828
34948
  });
33829
- server.tool("browser_cookies_clear", "Clear cookies from the current session", { session_id: exports_external.string(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
34949
+ server.tool("browser_cookies_clear", "Clear cookies from the current session", { session_id: exports_external.string().optional(), name: exports_external.string().optional(), domain: exports_external.string().optional() }, async ({ session_id, name, domain }) => {
33830
34950
  try {
33831
- const page = getSessionPage(session_id);
34951
+ const sid = resolveSessionId(session_id);
34952
+ const page = getSessionPage(sid);
33832
34953
  await clearCookies(page, name || domain ? { name, domain } : undefined);
33833
34954
  return json({ cleared: true });
33834
34955
  } catch (e) {
33835
34956
  return err(e);
33836
34957
  }
33837
34958
  });
33838
- server.tool("browser_storage_get", "Get localStorage or sessionStorage values", { session_id: exports_external.string(), key: exports_external.string().optional(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, storage_type }) => {
34959
+ server.tool("browser_storage_get", "Get localStorage or sessionStorage values", { session_id: exports_external.string().optional(), key: exports_external.string().optional(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, storage_type }) => {
33839
34960
  try {
33840
- const page = getSessionPage(session_id);
34961
+ const sid = resolveSessionId(session_id);
34962
+ const page = getSessionPage(sid);
33841
34963
  const value = storage_type === "session" ? await getSessionStorage(page, key) : await getLocalStorage(page, key);
33842
34964
  return json({ value });
33843
34965
  } catch (e) {
33844
34966
  return err(e);
33845
34967
  }
33846
34968
  });
33847
- server.tool("browser_storage_set", "Set a localStorage or sessionStorage value", { session_id: exports_external.string(), key: exports_external.string(), value: exports_external.string(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, value, storage_type }) => {
34969
+ server.tool("browser_storage_set", "Set a localStorage or sessionStorage value", { session_id: exports_external.string().optional(), key: exports_external.string(), value: exports_external.string(), storage_type: exports_external.enum(["local", "session"]).optional().default("local") }, async ({ session_id, key, value, storage_type }) => {
33848
34970
  try {
33849
- const page = getSessionPage(session_id);
34971
+ const sid = resolveSessionId(session_id);
34972
+ const page = getSessionPage(sid);
33850
34973
  if (storage_type === "session") {
33851
34974
  await setSessionStorage(page, key, value);
33852
34975
  } else {
@@ -33857,28 +34980,30 @@ var init_mcp = __esm(async () => {
33857
34980
  return err(e);
33858
34981
  }
33859
34982
  });
33860
- server.tool("browser_network_log", "Get captured network requests for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
34983
+ server.tool("browser_network_log", "Get captured network requests for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33861
34984
  try {
33862
- if (!networkLogCleanup.has(session_id)) {
33863
- const page = getSessionPage(session_id);
33864
- const cleanup = enableNetworkLogging(page, session_id);
33865
- networkLogCleanup.set(session_id, cleanup);
34985
+ const sid = resolveSessionId(session_id);
34986
+ if (!networkLogCleanup.has(sid)) {
34987
+ const page = getSessionPage(sid);
34988
+ const cleanup = enableNetworkLogging(page, sid);
34989
+ networkLogCleanup.set(sid, cleanup);
33866
34990
  }
33867
- const log = getNetworkLog(session_id);
34991
+ const log = getNetworkLog(sid);
33868
34992
  return json({ requests: log, count: log.length });
33869
34993
  } catch (e) {
33870
34994
  return err(e);
33871
34995
  }
33872
34996
  });
33873
34997
  server.tool("browser_network_intercept", "Add a network interception rule", {
33874
- session_id: exports_external.string(),
34998
+ session_id: exports_external.string().optional(),
33875
34999
  pattern: exports_external.string(),
33876
35000
  action: exports_external.enum(["block", "modify", "log"]),
33877
35001
  response_status: exports_external.number().optional(),
33878
35002
  response_body: exports_external.string().optional()
33879
35003
  }, async ({ session_id, pattern, action, response_status, response_body }) => {
33880
35004
  try {
33881
- const page = getSessionPage(session_id);
35005
+ const sid = resolveSessionId(session_id);
35006
+ const page = getSessionPage(sid);
33882
35007
  await addInterceptRule(page, {
33883
35008
  pattern,
33884
35009
  action,
@@ -33889,27 +35014,29 @@ var init_mcp = __esm(async () => {
33889
35014
  return err(e);
33890
35015
  }
33891
35016
  });
33892
- server.tool("browser_har_start", "Start HAR capture for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
35017
+ server.tool("browser_har_start", "Start HAR capture for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33893
35018
  try {
33894
- const page = getSessionPage(session_id);
35019
+ const sid = resolveSessionId(session_id);
35020
+ const page = getSessionPage(sid);
33895
35021
  const capture = startHAR(page);
33896
- harCaptures.set(session_id, capture);
35022
+ harCaptures.set(sid, capture);
33897
35023
  return json({ started: true });
33898
35024
  } catch (e) {
33899
35025
  return err(e);
33900
35026
  }
33901
35027
  });
33902
- server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { session_id: exports_external.string() }, async ({ session_id }) => {
35028
+ server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33903
35029
  try {
33904
- const capture = harCaptures.get(session_id);
35030
+ const sid = resolveSessionId(session_id);
35031
+ const capture = harCaptures.get(sid);
33905
35032
  if (!capture)
33906
35033
  return err(new Error("No active HAR capture for this session"));
33907
35034
  const har = capture.stop();
33908
- harCaptures.delete(session_id);
35035
+ harCaptures.delete(sid);
33909
35036
  let download_id;
33910
35037
  try {
33911
35038
  const harBuf = Buffer.from(JSON.stringify(har, null, 2));
33912
- const dl = saveToDownloads(harBuf, `capture-${Date.now()}.har`, { sessionId: session_id, type: "har" });
35039
+ const dl = saveToDownloads(harBuf, `capture-${Date.now()}.har`, { sessionId: sid, type: "har" });
33913
35040
  download_id = dl.id;
33914
35041
  } catch {}
33915
35042
  return json({ har, entry_count: har.log.entries.length, download_id });
@@ -33917,32 +35044,35 @@ var init_mcp = __esm(async () => {
33917
35044
  return err(e);
33918
35045
  }
33919
35046
  });
33920
- server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string() }, async ({ session_id }) => {
35047
+ server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
33921
35048
  try {
33922
- const page = getSessionPage(session_id);
35049
+ const sid = resolveSessionId(session_id);
35050
+ const page = getSessionPage(sid);
33923
35051
  const metrics = await getPerformanceMetrics(page);
33924
35052
  return json({ metrics });
33925
35053
  } catch (e) {
33926
35054
  return err(e);
33927
35055
  }
33928
35056
  });
33929
- server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
35057
+ server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
33930
35058
  try {
33931
- if (!consoleCaptureCleanup.has(session_id)) {
33932
- const page = getSessionPage(session_id);
33933
- const cleanup = enableConsoleCapture(page, session_id);
33934
- consoleCaptureCleanup.set(session_id, cleanup);
35059
+ const sid = resolveSessionId(session_id);
35060
+ if (!consoleCaptureCleanup.has(sid)) {
35061
+ const page = getSessionPage(sid);
35062
+ const cleanup = enableConsoleCapture(page, sid);
35063
+ consoleCaptureCleanup.set(sid, cleanup);
33935
35064
  }
33936
- const messages = getConsoleLog(session_id, level);
35065
+ const messages = getConsoleLog(sid, level);
33937
35066
  return json({ messages, count: messages.length });
33938
35067
  } catch (e) {
33939
35068
  return err(e);
33940
35069
  }
33941
35070
  });
33942
- server.tool("browser_record_start", "Start recording actions in a session", { session_id: exports_external.string(), name: exports_external.string(), project_id: exports_external.string().optional() }, async ({ session_id, name }) => {
35071
+ server.tool("browser_record_start", "Start recording actions in a session", { session_id: exports_external.string().optional(), name: exports_external.string(), project_id: exports_external.string().optional() }, async ({ session_id, name }) => {
33943
35072
  try {
33944
- const page = getSessionPage(session_id);
33945
- const recording = startRecording(session_id, name, page.url());
35073
+ const sid = resolveSessionId(session_id);
35074
+ const page = getSessionPage(sid);
35075
+ const recording = startRecording(sid, name, page.url());
33946
35076
  return json({ recording_id: recording.id, name: recording.name });
33947
35077
  } catch (e) {
33948
35078
  return err(e);
@@ -33970,9 +35100,10 @@ var init_mcp = __esm(async () => {
33970
35100
  return err(e);
33971
35101
  }
33972
35102
  });
33973
- server.tool("browser_record_replay", "Replay a recorded sequence in a session", { session_id: exports_external.string(), recording_id: exports_external.string() }, async ({ session_id, recording_id }) => {
35103
+ server.tool("browser_record_replay", "Replay a recorded sequence in a session", { session_id: exports_external.string().optional(), recording_id: exports_external.string() }, async ({ session_id, recording_id }) => {
33974
35104
  try {
33975
- const page = getSessionPage(session_id);
35105
+ const sid = resolveSessionId(session_id);
35106
+ const page = getSessionPage(sid);
33976
35107
  const result = await replayRecording(recording_id, page);
33977
35108
  return json(result);
33978
35109
  } catch (e) {
@@ -34010,7 +35141,7 @@ var init_mcp = __esm(async () => {
34010
35141
  server.tool("browser_register_agent", "Register an agent with the browser service", {
34011
35142
  name: exports_external.string(),
34012
35143
  description: exports_external.string().optional(),
34013
- session_id: exports_external.string().optional(),
35144
+ session_id: exports_external.string().optional().optional(),
34014
35145
  project_id: exports_external.string().optional(),
34015
35146
  working_dir: exports_external.string().optional()
34016
35147
  }, async ({ name, description, session_id, project_id, working_dir }) => {
@@ -34051,9 +35182,10 @@ var init_mcp = __esm(async () => {
34051
35182
  return err(e);
34052
35183
  }
34053
35184
  });
34054
- server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
35185
+ server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string().optional(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
34055
35186
  try {
34056
- const page = getSessionPage(session_id);
35187
+ const sid = resolveSessionId(session_id);
35188
+ const page = getSessionPage(sid);
34057
35189
  await scroll(page, direction, amount);
34058
35190
  await new Promise((r) => setTimeout(r, wait_ms));
34059
35191
  const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
@@ -34068,9 +35200,10 @@ var init_mcp = __esm(async () => {
34068
35200
  return err(e);
34069
35201
  }
34070
35202
  });
34071
- server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
35203
+ server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string().optional(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
34072
35204
  try {
34073
- const page = getSessionPage(session_id);
35205
+ const sid = resolveSessionId(session_id);
35206
+ const page = getSessionPage(sid);
34074
35207
  const start = Date.now();
34075
35208
  if (url_pattern) {
34076
35209
  await page.waitForURL(url_pattern, { timeout });
@@ -34092,38 +35225,200 @@ var init_mcp = __esm(async () => {
34092
35225
  return err(e);
34093
35226
  }
34094
35227
  });
34095
- server.tool("browser_session_rename", "Rename a browser session", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
35228
+ server.tool("browser_session_rename", "Rename a browser session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
34096
35229
  try {
34097
- return json({ session: renameSession2(session_id, name) });
35230
+ const sid = resolveSessionId(session_id);
35231
+ return json({ session: renameSession2(sid, name) });
34098
35232
  } catch (e) {
34099
35233
  return err(e);
34100
35234
  }
34101
35235
  });
34102
- server.tool("browser_click_text", "Click an element by its visible text content", { session_id: exports_external.string(), text: exports_external.string(), exact: exports_external.boolean().optional().default(false), timeout: exports_external.number().optional() }, async ({ session_id, text, exact, timeout }) => {
35236
+ server.tool("browser_session_lock", "Lock a session so only the specified agent can use it", { session_id: exports_external.string().optional(), agent_id: exports_external.string() }, async ({ session_id, agent_id }) => {
34103
35237
  try {
34104
- const page = getSessionPage(session_id);
35238
+ const sid = resolveSessionId(session_id);
35239
+ const { lockSession: lockSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
35240
+ return json({ session: lockSession2(sid, agent_id) });
35241
+ } catch (e) {
35242
+ return err(e);
35243
+ }
35244
+ });
35245
+ server.tool("browser_session_unlock", "Unlock a session", { session_id: exports_external.string().optional(), agent_id: exports_external.string().optional() }, async ({ session_id, agent_id }) => {
35246
+ try {
35247
+ const sid = resolveSessionId(session_id);
35248
+ const { unlockSession: unlockSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
35249
+ return json({ session: unlockSession2(sid, agent_id) });
35250
+ } catch (e) {
35251
+ return err(e);
35252
+ }
35253
+ });
35254
+ server.tool("browser_session_transfer", "Transfer session ownership to another agent", { session_id: exports_external.string().optional(), to_agent_id: exports_external.string() }, async ({ session_id, to_agent_id }) => {
35255
+ try {
35256
+ const sid = resolveSessionId(session_id);
35257
+ const { transferSession: transferSession2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
35258
+ return json({ session: transferSession2(sid, to_agent_id) });
35259
+ } catch (e) {
35260
+ return err(e);
35261
+ }
35262
+ });
35263
+ server.tool("browser_session_tag", "Add a tag to a session for categorization (e.g. qa, scraping, monitoring)", { session_id: exports_external.string().optional(), tag: exports_external.string() }, async ({ session_id, tag }) => {
35264
+ try {
35265
+ const sid = resolveSessionId(session_id);
35266
+ const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
35267
+ return json({ tags: addSessionTag2(sid, tag) });
35268
+ } catch (e) {
35269
+ return err(e);
35270
+ }
35271
+ });
35272
+ server.tool("browser_session_untag", "Remove a tag from a session", { session_id: exports_external.string().optional(), tag: exports_external.string() }, async ({ session_id, tag }) => {
35273
+ try {
35274
+ const sid = resolveSessionId(session_id);
35275
+ const { removeSessionTag: removeSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
35276
+ return json({ tags: removeSessionTag2(sid, tag) });
35277
+ } catch (e) {
35278
+ return err(e);
35279
+ }
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
+ });
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 }) => {
35370
+ try {
35371
+ const sid = resolveSessionId(session_id);
35372
+ const page = getSessionPage(sid);
34105
35373
  await clickText(page, text, { exact, timeout });
34106
35374
  return json({ clicked: text });
34107
35375
  } catch (e) {
34108
35376
  return err(e);
34109
35377
  }
34110
35378
  });
34111
- server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
34112
- session_id: exports_external.string(),
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.", {
35380
+ session_id: exports_external.string().optional(),
34113
35381
  fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
34114
- submit_selector: exports_external.string().optional()
34115
- }, 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 }) => {
34116
35385
  try {
34117
- const page = getSessionPage(session_id);
34118
- const result = await fillForm(page, fields, submit_selector);
35386
+ const sid = resolveSessionId(session_id);
35387
+ const page = getSessionPage(sid);
35388
+ const result = await fillForm(page, fields, submit_selector, self_heal);
34119
35389
  return json(result);
35390
+ } catch (e) {
35391
+ return errWithScreenshot(e, session_id);
35392
+ }
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
+ }
34120
35414
  } catch (e) {
34121
35415
  return err(e);
34122
35416
  }
34123
35417
  });
34124
- server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
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 }) => {
34125
35419
  try {
34126
- const page = getSessionPage(session_id);
35420
+ const sid = resolveSessionId(session_id);
35421
+ const page = getSessionPage(sid);
34127
35422
  const start = Date.now();
34128
35423
  await waitForText(page, text, { timeout, exact });
34129
35424
  return json({ found: true, elapsed_ms: Date.now() - start });
@@ -34131,46 +35426,51 @@ var init_mcp = __esm(async () => {
34131
35426
  return err(e);
34132
35427
  }
34133
35428
  });
34134
- server.tool("browser_element_exists", "Check if a selector exists on the page (no throw, returns boolean)", { session_id: exports_external.string(), selector: exports_external.string(), check_visible: exports_external.boolean().optional().default(false) }, async ({ session_id, selector, check_visible }) => {
35429
+ server.tool("browser_element_exists", "Check if a selector exists on the page (no throw, returns boolean)", { session_id: exports_external.string().optional(), selector: exports_external.string(), check_visible: exports_external.boolean().optional().default(false) }, async ({ session_id, selector, check_visible }) => {
34135
35430
  try {
34136
- const page = getSessionPage(session_id);
35431
+ const sid = resolveSessionId(session_id);
35432
+ const page = getSessionPage(sid);
34137
35433
  return json(await elementExists(page, selector, { visible: check_visible }));
34138
35434
  } catch (e) {
34139
35435
  return err(e);
34140
35436
  }
34141
35437
  });
34142
- server.tool("browser_get_page_info", "Get a full page summary in one call: url, title, meta tags, link/image/form counts, text length", { session_id: exports_external.string() }, async ({ session_id }) => {
35438
+ server.tool("browser_get_page_info", "Get a full page summary in one call: url, title, meta tags, link/image/form counts, text length", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34143
35439
  try {
34144
- const page = getSessionPage(session_id);
35440
+ const sid = resolveSessionId(session_id);
35441
+ const page = getSessionPage(sid);
34145
35442
  const info = await getPageInfo(page);
34146
- const errors2 = getConsoleLog(session_id, "error");
35443
+ const errors2 = getConsoleLog(sid, "error");
34147
35444
  info.has_console_errors = errors2.length > 0;
34148
35445
  return json(info);
34149
35446
  } catch (e) {
34150
35447
  return err(e);
34151
35448
  }
34152
35449
  });
34153
- server.tool("browser_has_errors", "Quick check: does the session have any console errors?", { session_id: exports_external.string() }, async ({ session_id }) => {
35450
+ server.tool("browser_has_errors", "Quick check: does the session have any console errors?", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34154
35451
  try {
34155
- const errors2 = getConsoleLog(session_id, "error");
35452
+ const sid = resolveSessionId(session_id);
35453
+ const errors2 = getConsoleLog(sid, "error");
34156
35454
  return json({ has_errors: errors2.length > 0, error_count: errors2.length, errors: errors2 });
34157
35455
  } catch (e) {
34158
35456
  return err(e);
34159
35457
  }
34160
35458
  });
34161
- server.tool("browser_clear_errors", "Clear console error log for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
35459
+ server.tool("browser_clear_errors", "Clear console error log for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34162
35460
  try {
35461
+ const sid = resolveSessionId(session_id);
34163
35462
  const { clearConsoleLog: clearConsoleLog2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
34164
- clearConsoleLog2(session_id);
35463
+ clearConsoleLog2(sid);
34165
35464
  return json({ cleared: true });
34166
35465
  } catch (e) {
34167
35466
  return err(e);
34168
35467
  }
34169
35468
  });
34170
35469
  activeWatchHandles2 = new Map;
34171
- server.tool("browser_watch_start", "Start watching a page for DOM changes", { session_id: exports_external.string(), selector: exports_external.string().optional(), interval_ms: exports_external.number().optional().default(500), max_changes: exports_external.number().optional().default(50) }, async ({ session_id, selector, interval_ms, max_changes }) => {
35470
+ server.tool("browser_watch_start", "Start watching a page for DOM changes", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), interval_ms: exports_external.number().optional().default(500), max_changes: exports_external.number().optional().default(50) }, async ({ session_id, selector, interval_ms, max_changes }) => {
34172
35471
  try {
34173
- const page = getSessionPage(session_id);
35472
+ const sid = resolveSessionId(session_id);
35473
+ const page = getSessionPage(sid);
34174
35474
  const handle = watchPage(page, { selector, intervalMs: interval_ms, maxChanges: max_changes });
34175
35475
  activeWatchHandles2.set(handle.id, handle);
34176
35476
  return json({ watch_id: handle.id });
@@ -34197,7 +35497,7 @@ var init_mcp = __esm(async () => {
34197
35497
  });
34198
35498
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
34199
35499
  project_id: exports_external.string().optional(),
34200
- session_id: exports_external.string().optional(),
35500
+ session_id: exports_external.string().optional().optional(),
34201
35501
  tag: exports_external.string().optional(),
34202
35502
  is_favorite: exports_external.boolean().optional(),
34203
35503
  date_from: exports_external.string().optional(),
@@ -34285,14 +35585,14 @@ var init_mcp = __esm(async () => {
34285
35585
  return err(e);
34286
35586
  }
34287
35587
  });
34288
- server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
35588
+ server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external.string().optional().optional() }, async ({ session_id }) => {
34289
35589
  try {
34290
35590
  return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
34291
35591
  } catch (e) {
34292
35592
  return err(e);
34293
35593
  }
34294
35594
  });
34295
- server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, session_id }) => {
35595
+ server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, session_id }) => {
34296
35596
  try {
34297
35597
  const file = getDownload(id, session_id);
34298
35598
  if (!file)
@@ -34303,7 +35603,7 @@ var init_mcp = __esm(async () => {
34303
35603
  return err(e);
34304
35604
  }
34305
35605
  });
34306
- server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, session_id }) => {
35606
+ server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, session_id }) => {
34307
35607
  try {
34308
35608
  const deleted = deleteDownload(id, session_id);
34309
35609
  return json({ deleted });
@@ -34318,7 +35618,7 @@ var init_mcp = __esm(async () => {
34318
35618
  return err(e);
34319
35619
  }
34320
35620
  });
34321
- server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external.string(), target_path: exports_external.string(), session_id: exports_external.string().optional() }, async ({ id, target_path, session_id }) => {
35621
+ server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external.string(), target_path: exports_external.string(), session_id: exports_external.string().optional().optional() }, async ({ id, target_path, session_id }) => {
34322
35622
  try {
34323
35623
  const finalPath = exportToPath(id, target_path, session_id);
34324
35624
  return json({ path: finalPath });
@@ -34343,12 +35643,13 @@ var init_mcp = __esm(async () => {
34343
35643
  return err(e);
34344
35644
  }
34345
35645
  });
34346
- server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
35646
+ server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34347
35647
  try {
34348
- const page = getSessionPage(session_id);
34349
- const before = getLastSnapshot(session_id);
34350
- const after = await takeSnapshot(page, session_id);
34351
- setLastSnapshot(session_id, after);
35648
+ const sid = resolveSessionId(session_id);
35649
+ const page = getSessionPage(sid);
35650
+ const before = getLastSnapshot(sid);
35651
+ const after = await takeSnapshot(page, sid);
35652
+ setLastSnapshot(sid, after);
34352
35653
  if (!before) {
34353
35654
  return json({
34354
35655
  message: "No previous snapshot \u2014 returning current snapshot only.",
@@ -34371,12 +35672,13 @@ var init_mcp = __esm(async () => {
34371
35672
  return err(e);
34372
35673
  }
34373
35674
  });
34374
- server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
35675
+ server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34375
35676
  try {
34376
- const session = getSession2(session_id);
34377
- const networkLog = getNetworkLog(session_id);
34378
- const consoleLog = getConsoleLog(session_id);
34379
- const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
35677
+ const sid = resolveSessionId(session_id);
35678
+ const session = getSession2(sid);
35679
+ const networkLog = getNetworkLog(sid);
35680
+ const consoleLog = getConsoleLog(sid);
35681
+ const galleryEntries = listEntries({ sessionId: sid, limit: 1000 });
34380
35682
  let totalChars = 0;
34381
35683
  for (const req of networkLog) {
34382
35684
  totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
@@ -34388,7 +35690,7 @@ var init_mcp = __esm(async () => {
34388
35690
  totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
34389
35691
  }
34390
35692
  const estimatedTokens = Math.ceil(totalChars / 4);
34391
- const tokenBudget = getTokenBudget(session_id);
35693
+ const tokenBudget = getTokenBudget(sid);
34392
35694
  return json({
34393
35695
  session,
34394
35696
  network_request_count: networkLog.length,
@@ -34402,52 +35704,57 @@ var init_mcp = __esm(async () => {
34402
35704
  return err(e);
34403
35705
  }
34404
35706
  });
34405
- server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
35707
+ server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string().optional(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
34406
35708
  try {
34407
- const page = getSessionPage(session_id);
35709
+ const sid = resolveSessionId(session_id);
35710
+ const page = getSessionPage(sid);
34408
35711
  const tab = await newTab(page, url);
34409
35712
  return json(tab);
34410
35713
  } catch (e) {
34411
35714
  return err(e);
34412
35715
  }
34413
35716
  });
34414
- server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
35717
+ server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34415
35718
  try {
34416
- const page = getSessionPage(session_id);
35719
+ const sid = resolveSessionId(session_id);
35720
+ const page = getSessionPage(sid);
34417
35721
  const tabs = await listTabs(page);
34418
35722
  return json({ tabs, count: tabs.length });
34419
35723
  } catch (e) {
34420
35724
  return err(e);
34421
35725
  }
34422
35726
  });
34423
- server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
35727
+ server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string().optional(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
34424
35728
  try {
34425
- const page = getSessionPage(session_id);
35729
+ const sid = resolveSessionId(session_id);
35730
+ const page = getSessionPage(sid);
34426
35731
  const result = await switchTab(page, tab_id);
34427
- setSessionPage(session_id, result.page);
35732
+ setSessionPage(sid, result.page);
34428
35733
  return json(result.tab);
34429
35734
  } catch (e) {
34430
35735
  return err(e);
34431
35736
  }
34432
35737
  });
34433
- server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
35738
+ server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string().optional(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
34434
35739
  try {
34435
- const page = getSessionPage(session_id);
35740
+ const sid = resolveSessionId(session_id);
35741
+ const page = getSessionPage(sid);
34436
35742
  const context = page.context();
34437
35743
  const result = await closeTab(page, tab_id);
34438
35744
  const remainingPages = context.pages();
34439
35745
  const newActivePage = remainingPages[result.active_tab.index];
34440
35746
  if (newActivePage) {
34441
- setSessionPage(session_id, newActivePage);
35747
+ setSessionPage(sid, newActivePage);
34442
35748
  }
34443
35749
  return json(result);
34444
35750
  } catch (e) {
34445
35751
  return err(e);
34446
35752
  }
34447
35753
  });
34448
- server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
35754
+ server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string().optional(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
34449
35755
  try {
34450
- const result = await handleDialog(session_id, action, prompt_text);
35756
+ const sid = resolveSessionId(session_id);
35757
+ const result = await handleDialog(sid, action, prompt_text);
34451
35758
  if (!result.handled)
34452
35759
  return err(new Error("No pending dialogs for this session"));
34453
35760
  return json(result);
@@ -34455,28 +35762,31 @@ var init_mcp = __esm(async () => {
34455
35762
  return err(e);
34456
35763
  }
34457
35764
  });
34458
- server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
35765
+ server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34459
35766
  try {
34460
- const dialogs = getDialogs(session_id);
35767
+ const sid = resolveSessionId(session_id);
35768
+ const dialogs = getDialogs(sid);
34461
35769
  return json({ dialogs, count: dialogs.length });
34462
35770
  } catch (e) {
34463
35771
  return err(e);
34464
35772
  }
34465
35773
  });
34466
- server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
35774
+ server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
34467
35775
  try {
34468
- const page = getSessionPage(session_id);
35776
+ const sid = resolveSessionId(session_id);
35777
+ const page = getSessionPage(sid);
34469
35778
  const info = await saveProfile(page, name);
34470
35779
  return json(info);
34471
35780
  } catch (e) {
34472
35781
  return err(e);
34473
35782
  }
34474
35783
  });
34475
- server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
35784
+ server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
34476
35785
  try {
34477
35786
  const profileData = loadProfile(name);
34478
35787
  if (session_id) {
34479
- const page = getSessionPage(session_id);
35788
+ const sid = resolveSessionId(session_id);
35789
+ const page = getSessionPage(sid);
34480
35790
  const applied = await applyProfile(page, profileData);
34481
35791
  return json({ ...applied, profile: name });
34482
35792
  }
@@ -34525,6 +35835,7 @@ var init_mcp = __esm(async () => {
34525
35835
  { tool: "browser_wait", description: "Wait for a selector to appear" },
34526
35836
  { tool: "browser_wait_for_text", description: "Wait for text to appear" },
34527
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)" },
34528
35839
  { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
34529
35840
  ],
34530
35841
  Extraction: [
@@ -34553,7 +35864,10 @@ var init_mcp = __esm(async () => {
34553
35864
  { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
34554
35865
  { tool: "browser_profile_load", description: "Load and apply a saved profile" },
34555
35866
  { tool: "browser_profile_list", description: "List saved profiles" },
34556
- { 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" }
34557
35871
  ],
34558
35872
  Network: [
34559
35873
  { tool: "browser_network_log", description: "Get captured network requests" },
@@ -34577,6 +35891,13 @@ var init_mcp = __esm(async () => {
34577
35891
  { tool: "browser_record_replay", description: "Replay a recorded sequence" },
34578
35892
  { tool: "browser_recordings_list", description: "List all recordings" }
34579
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
+ ],
34580
35901
  Crawl: [
34581
35902
  { tool: "browser_crawl", description: "Crawl a URL recursively" }
34582
35903
  ],
@@ -34614,7 +35935,13 @@ var init_mcp = __esm(async () => {
34614
35935
  { tool: "browser_session_close", description: "Close a session" },
34615
35936
  { tool: "browser_session_get_by_name", description: "Get session by name" },
34616
35937
  { tool: "browser_session_rename", description: "Rename a session" },
35938
+ { tool: "browser_session_lock", description: "Lock a session for an agent" },
35939
+ { tool: "browser_session_unlock", description: "Unlock a session" },
35940
+ { tool: "browser_session_transfer", description: "Transfer session to another agent" },
35941
+ { tool: "browser_session_tag", description: "Add a tag to a session" },
35942
+ { tool: "browser_session_untag", description: "Remove a tag from a session" },
34617
35943
  { tool: "browser_session_stats", description: "Get session stats and token usage" },
35944
+ { tool: "browser_session_timeline", description: "Get chronological action log" },
34618
35945
  { tool: "browser_tab_new", description: "Open a new tab" },
34619
35946
  { tool: "browser_tab_list", description: "List all open tabs" },
34620
35947
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
@@ -34627,7 +35954,8 @@ var init_mcp = __esm(async () => {
34627
35954
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
34628
35955
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
34629
35956
  { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
34630
- { 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" }
34631
35959
  ]
34632
35960
  };
34633
35961
  const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
@@ -34652,18 +35980,19 @@ var init_mcp = __esm(async () => {
34652
35980
  }
34653
35981
  });
34654
35982
  server.tool("browser_scroll_to_element", "Scroll an element into view (by ref or selector) then optionally take a screenshot of it. Replaces scroll + wait + screenshot pattern.", {
34655
- session_id: exports_external.string(),
35983
+ session_id: exports_external.string().optional(),
34656
35984
  selector: exports_external.string().optional(),
34657
35985
  ref: exports_external.string().optional(),
34658
35986
  screenshot: exports_external.boolean().optional().default(true),
34659
35987
  wait_ms: exports_external.number().optional().default(200)
34660
35988
  }, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
34661
35989
  try {
34662
- const page = getSessionPage(session_id);
35990
+ const sid = resolveSessionId(session_id);
35991
+ const page = getSessionPage(sid);
34663
35992
  let locator;
34664
35993
  if (ref) {
34665
35994
  const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
34666
- locator = getRefLocator2(page, session_id, ref);
35995
+ locator = getRefLocator2(page, sid, ref);
34667
35996
  } else if (selector) {
34668
35997
  locator = page.locator(selector).first();
34669
35998
  } else {
@@ -34688,11 +36017,12 @@ var init_mcp = __esm(async () => {
34688
36017
  return err(e);
34689
36018
  }
34690
36019
  });
34691
- server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
36020
+ server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
34692
36021
  try {
34693
- const page = getSessionPage(session_id);
36022
+ const sid = resolveSessionId(session_id);
36023
+ const page = getSessionPage(sid);
34694
36024
  const info = await getPageInfo(page);
34695
- const errors2 = getConsoleLog(session_id, "error");
36025
+ const errors2 = getConsoleLog(sid, "error");
34696
36026
  info.has_console_errors = errors2.length > 0;
34697
36027
  let perf = {};
34698
36028
  try {
@@ -34706,8 +36036,8 @@ var init_mcp = __esm(async () => {
34706
36036
  let snapshot_refs = "";
34707
36037
  let interactive_count = 0;
34708
36038
  try {
34709
- const snap = await takeSnapshot(page, session_id);
34710
- setLastSnapshot(session_id, snap);
36039
+ const snap = await takeSnapshot(page, sid);
36040
+ setLastSnapshot(sid, snap);
34711
36041
  interactive_count = snap.interactive_count;
34712
36042
  snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
34713
36043
  } catch {}
@@ -34716,9 +36046,10 @@ var init_mcp = __esm(async () => {
34716
36046
  return err(e);
34717
36047
  }
34718
36048
  });
34719
- server.tool("browser_secrets_login", "Login to a service using credentials from open-secrets vault or ~/.secrets. One call replaces 10+ tool calls.", { session_id: exports_external.string(), service: exports_external.string(), login_url: exports_external.string().optional(), save_profile: exports_external.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
36049
+ server.tool("browser_secrets_login", "Login to a service using credentials from open-secrets vault or ~/.secrets. One call replaces 10+ tool calls.", { session_id: exports_external.string().optional(), service: exports_external.string(), login_url: exports_external.string().optional(), save_profile: exports_external.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
34720
36050
  try {
34721
- const page = getSessionPage(session_id);
36051
+ const sid = resolveSessionId(session_id);
36052
+ const page = getSessionPage(sid);
34722
36053
  const { getCredentials: getCredentials2, loginWithCredentials: loginWithCredentials2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
34723
36054
  const creds = await getCredentials2(service);
34724
36055
  if (!creds)
@@ -34732,9 +36063,10 @@ var init_mcp = __esm(async () => {
34732
36063
  return err(e);
34733
36064
  }
34734
36065
  });
34735
- server.tool("browser_remember", "Store page facts in open-mementos for future recall. Agents skip re-scraping on repeat visits.", { session_id: exports_external.string(), facts: exports_external.record(exports_external.unknown()), tags: exports_external.array(exports_external.string()).optional() }, async ({ session_id, facts, tags }) => {
36066
+ server.tool("browser_remember", "Store page facts in open-mementos for future recall. Agents skip re-scraping on repeat visits.", { session_id: exports_external.string().optional(), facts: exports_external.record(exports_external.unknown()), tags: exports_external.array(exports_external.string()).optional() }, async ({ session_id, facts, tags }) => {
34736
36067
  try {
34737
- const page = getSessionPage(session_id);
36068
+ const sid = resolveSessionId(session_id);
36069
+ const page = getSessionPage(sid);
34738
36070
  const { rememberPage: rememberPage2 } = await Promise.resolve().then(() => (init_page_memory(), exports_page_memory));
34739
36071
  const url = page.url();
34740
36072
  await rememberPage2(url, facts, tags);
@@ -34752,12 +36084,13 @@ var init_mcp = __esm(async () => {
34752
36084
  return err(e);
34753
36085
  }
34754
36086
  });
34755
- server.tool("browser_session_announce", "Announce to other agents via open-conversations what this session is browsing.", { session_id: exports_external.string(), message: exports_external.string().optional() }, async ({ session_id, message }) => {
36087
+ server.tool("browser_session_announce", "Announce to other agents via open-conversations what this session is browsing.", { session_id: exports_external.string().optional(), message: exports_external.string().optional() }, async ({ session_id, message }) => {
34756
36088
  try {
34757
- const page = getSessionPage(session_id);
36089
+ const sid = resolveSessionId(session_id);
36090
+ const page = getSessionPage(sid);
34758
36091
  const { announceNavigation: announceNavigation2 } = await Promise.resolve().then(() => (init_coordination(), exports_coordination));
34759
36092
  const url = page.url();
34760
- await announceNavigation2(url, session_id);
36093
+ await announceNavigation2(url, sid);
34761
36094
  return json({ announced: true, url, message });
34762
36095
  } catch (e) {
34763
36096
  return err(e);
@@ -34797,9 +36130,10 @@ var init_mcp = __esm(async () => {
34797
36130
  return err(e);
34798
36131
  }
34799
36132
  });
34800
- server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing, extract-nav-links, monitor-price, get-metadata). One call replaces 5\u201315 tool calls.", { session_id: exports_external.string(), skill: exports_external.string(), params: exports_external.record(exports_external.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
36133
+ server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing, extract-nav-links, monitor-price, get-metadata). One call replaces 5\u201315 tool calls.", { session_id: exports_external.string().optional(), skill: exports_external.string(), params: exports_external.record(exports_external.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
34801
36134
  try {
34802
- const page = getSessionPage(session_id);
36135
+ const sid = resolveSessionId(session_id);
36136
+ const page = getSessionPage(sid);
34803
36137
  const { runBrowserSkill: runBrowserSkill2 } = await Promise.resolve().then(() => (init_skills_runner(), exports_skills_runner));
34804
36138
  return json(await runBrowserSkill2(skill, params, page));
34805
36139
  } catch (e) {
@@ -34815,7 +36149,7 @@ var init_mcp = __esm(async () => {
34815
36149
  }
34816
36150
  });
34817
36151
  server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot. Eliminates 80% of round trips for multi-step flows.", {
34818
- session_id: exports_external.string(),
36152
+ session_id: exports_external.string().optional(),
34819
36153
  actions: exports_external.array(exports_external.object({
34820
36154
  tool: exports_external.string(),
34821
36155
  args: exports_external.record(exports_external.unknown()).optional().default({})
@@ -34823,12 +36157,13 @@ var init_mcp = __esm(async () => {
34823
36157
  }, async ({ session_id, actions }) => {
34824
36158
  try {
34825
36159
  const results = [];
34826
- const page = getSessionPage(session_id);
36160
+ const sid = resolveSessionId(session_id);
36161
+ const page = getSessionPage(sid);
34827
36162
  const t0 = Date.now();
34828
36163
  for (const action of actions) {
34829
36164
  try {
34830
36165
  const toolName = action.tool.replace(/^browser_/, "");
34831
- const args = { session_id, ...action.args };
36166
+ const args = { session_id: sid, ...action.args };
34832
36167
  switch (toolName) {
34833
36168
  case "navigate":
34834
36169
  await navigate(page, action.args.url);
@@ -34837,7 +36172,7 @@ var init_mcp = __esm(async () => {
34837
36172
  case "click":
34838
36173
  if (args.ref) {
34839
36174
  const { clickRef: clickRef2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
34840
- await clickRef2(page, session_id, args.ref);
36175
+ await clickRef2(page, sid, args.ref);
34841
36176
  } else if (args.selector)
34842
36177
  await page.click(args.selector);
34843
36178
  results.push({ tool: action.tool, success: true });
@@ -34845,7 +36180,7 @@ var init_mcp = __esm(async () => {
34845
36180
  case "type":
34846
36181
  if (args.ref && args.text) {
34847
36182
  const { typeRef: typeRef2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
34848
- await typeRef2(page, session_id, args.ref, args.text);
36183
+ await typeRef2(page, sid, args.ref, args.text);
34849
36184
  } else if (args.selector && args.text)
34850
36185
  await page.fill(args.selector, args.text);
34851
36186
  results.push({ tool: action.tool, success: true });
@@ -34885,7 +36220,7 @@ var init_mcp = __esm(async () => {
34885
36220
  }
34886
36221
  let final_snapshot = {};
34887
36222
  try {
34888
- const snap = await takeSnapshot(page, session_id);
36223
+ const snap = await takeSnapshot(page, sid);
34889
36224
  final_snapshot = {
34890
36225
  refs: Object.fromEntries(Object.entries(snap.refs).slice(0, 20)),
34891
36226
  interactive_count: snap.interactive_count
@@ -34903,6 +36238,82 @@ var init_mcp = __esm(async () => {
34903
36238
  return err(e);
34904
36239
  }
34905
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
+ });
34906
36317
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
34907
36318
  try {
34908
36319
  return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
@@ -34982,18 +36393,20 @@ var init_mcp = __esm(async () => {
34982
36393
  return err(e);
34983
36394
  }
34984
36395
  });
34985
- server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku. Returns result + steps taken.", { session_id: exports_external.string(), task: exports_external.string(), max_steps: exports_external.number().optional().default(10), model: exports_external.string().optional() }, async ({ session_id, task, max_steps, model }) => {
36396
+ server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku. Returns result + steps taken.", { session_id: exports_external.string().optional(), task: exports_external.string(), max_steps: exports_external.number().optional().default(10), model: exports_external.string().optional() }, async ({ session_id, task, max_steps, model }) => {
34986
36397
  try {
34987
- const page = getSessionPage(session_id);
36398
+ const sid = resolveSessionId(session_id);
36399
+ const page = getSessionPage(sid);
34988
36400
  const { executeBrowserTask: executeBrowserTask2 } = await Promise.resolve().then(() => (init_ai_task(), exports_ai_task));
34989
- return json(await executeBrowserTask2(page, task, { maxSteps: max_steps, model, sessionId: session_id }));
36401
+ return json(await executeBrowserTask2(page, task, { maxSteps: max_steps, model, sessionId: sid }));
34990
36402
  } catch (e) {
34991
36403
  return err(e);
34992
36404
  }
34993
36405
  });
34994
- server.tool("browser_assert", `Assert page conditions in one call. Conditions: 'url contains X', 'text:"Y" is visible', 'element:"#id" exists', 'count:"a" > 10', 'title contains Z'. Chain with AND.`, { session_id: exports_external.string(), condition: exports_external.string() }, async ({ session_id, condition }) => {
36406
+ server.tool("browser_assert", `Assert page conditions in one call. Conditions: 'url contains X', 'text:"Y" is visible', 'element:"#id" exists', 'count:"a" > 10', 'title contains Z'. Chain with AND.`, { session_id: exports_external.string().optional(), condition: exports_external.string() }, async ({ session_id, condition }) => {
34995
36407
  try {
34996
- const page = getSessionPage(session_id);
36408
+ const sid = resolveSessionId(session_id);
36409
+ const page = getSessionPage(sid);
34997
36410
  const checks = [];
34998
36411
  let passed = true;
34999
36412
  for (const part of condition.split(/\s+AND\s+/i)) {
@@ -35055,10 +36468,10 @@ __export(exports_snapshots, {
35055
36468
  deleteSnapshot: () => deleteSnapshot,
35056
36469
  createSnapshot: () => createSnapshot
35057
36470
  });
35058
- import { randomUUID as randomUUID13 } from "crypto";
36471
+ import { randomUUID as randomUUID14 } from "crypto";
35059
36472
  function createSnapshot(data) {
35060
36473
  const db2 = getDatabase();
35061
- const id = randomUUID13();
36474
+ const id = randomUUID14();
35062
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);
35063
36476
  return getSnapshot(id);
35064
36477
  }
@@ -35084,8 +36497,8 @@ var init_snapshots = __esm(() => {
35084
36497
 
35085
36498
  // src/server/index.ts
35086
36499
  var exports_server = {};
35087
- import { join as join14 } from "path";
35088
- import { existsSync as existsSync8 } from "fs";
36500
+ import { join as join15 } from "path";
36501
+ import { existsSync as existsSync9 } from "fs";
35089
36502
  function ok(data, status = 200) {
35090
36503
  return new Response(JSON.stringify(data), {
35091
36504
  status,
@@ -35335,14 +36748,14 @@ var init_server = __esm(() => {
35335
36748
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
35336
36749
  const id = path.split("/")[3];
35337
36750
  const entry = getEntry(id);
35338
- if (!entry?.thumbnail_path || !existsSync8(entry.thumbnail_path))
36751
+ if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
35339
36752
  return notFound("Thumbnail not found");
35340
36753
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
35341
36754
  }
35342
36755
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
35343
36756
  const id = path.split("/")[3];
35344
36757
  const entry = getEntry(id);
35345
- if (!entry?.path || !existsSync8(entry.path))
36758
+ if (!entry?.path || !existsSync9(entry.path))
35346
36759
  return notFound("Image not found");
35347
36760
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
35348
36761
  }
@@ -35370,7 +36783,7 @@ var init_server = __esm(() => {
35370
36783
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
35371
36784
  const id = path.split("/")[3];
35372
36785
  const file = getDownload(id);
35373
- if (!file || !existsSync8(file.path))
36786
+ if (!file || !existsSync9(file.path))
35374
36787
  return notFound("Download not found");
35375
36788
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
35376
36789
  }
@@ -35378,13 +36791,13 @@ var init_server = __esm(() => {
35378
36791
  const id = path.split("/")[3];
35379
36792
  return ok({ deleted: deleteDownload(id) });
35380
36793
  }
35381
- const dashboardDist = join14(import.meta.dir, "../../dashboard/dist");
35382
- if (existsSync8(dashboardDist)) {
35383
- const filePath = path === "/" ? join14(dashboardDist, "index.html") : join14(dashboardDist, path);
35384
- 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)) {
35385
36798
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
35386
36799
  }
35387
- 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 });
35388
36801
  }
35389
36802
  if (path === "/" || path === "") {
35390
36803
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -35427,31 +36840,71 @@ init_recorder();
35427
36840
  init_recordings();
35428
36841
  init_lightpanda();
35429
36842
  import { readFileSync as readFileSync9 } from "fs";
35430
- import { join as join15 } from "path";
36843
+ import { join as join16 } from "path";
35431
36844
  import chalk from "chalk";
35432
- 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"));
35433
36846
  var program2 = new Command;
35434
36847
  program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
35435
- 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("--headless", "Run in headless mode (default: true)", true).action(async (url, opts) => {
35436
- const { session, page } = await createSession2({ engine: opts.engine, headless: true });
35437
- console.log(chalk.gray(`Session: ${session.id} (${session.engine})`));
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) => {
36849
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
35438
36850
  await navigate(page, url);
35439
36851
  const title = await page.title();
35440
- console.log(chalk.green(`\u2713 Navigated to: ${url}`));
35441
- console.log(chalk.blue(` Title: ${title}`));
36852
+ let screenshotPath;
35442
36853
  if (opts.screenshot) {
35443
36854
  const result = await takeScreenshot(page);
35444
- console.log(chalk.blue(` Screenshot: ${result.path}`));
36855
+ screenshotPath = result.path;
35445
36856
  }
36857
+ let text;
35446
36858
  if (opts.extract) {
35447
- const text = await getText(page);
35448
- console.log(chalk.white(`
36859
+ text = await getText(page);
36860
+ }
36861
+ if (opts.json) {
36862
+ const output = { session_id: session.id, engine: session.engine, url, title };
36863
+ if (screenshotPath)
36864
+ output.screenshot = screenshotPath;
36865
+ if (text)
36866
+ output.text = text.slice(0, 500);
36867
+ console.log(JSON.stringify(output, null, 2));
36868
+ } else {
36869
+ console.log(chalk.gray(`Session: ${session.id} (${session.engine})`));
36870
+ console.log(chalk.green(`\u2713 Navigated to: ${url}`));
36871
+ console.log(chalk.blue(` Title: ${title}`));
36872
+ if (screenshotPath)
36873
+ console.log(chalk.blue(` Screenshot: ${screenshotPath}`));
36874
+ if (text)
36875
+ console.log(chalk.white(`
35449
36876
  ${text.slice(0, 500)}...`));
35450
36877
  }
35451
36878
  await closeSession2(session.id);
35452
36879
  });
35453
- program2.command("screenshot <url>").description("Navigate to a URL and take a screenshot").option("--engine <engine>", "Browser engine", "auto").option("--selector <selector>", "CSS selector for element screenshot").option("--full-page", "Capture full page").option("--format <format>", "Image format: png|jpeg|webp", "png").action(async (url, opts) => {
35454
- const { session, page } = await createSession2({ engine: opts.engine, headless: true });
36880
+ program2.command("check <url>").description("One-liner page health check: navigate, screenshot, extract info, check errors").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
36881
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
36882
+ await navigate(page, url);
36883
+ const title = await page.title();
36884
+ const currentUrl = page.url();
36885
+ const text = await getText(page);
36886
+ const links = await getLinks(page);
36887
+ const result = await takeScreenshot(page);
36888
+ const summary = {
36889
+ url: currentUrl,
36890
+ title,
36891
+ text_length: text.length,
36892
+ links_count: links.length,
36893
+ screenshot: result.path,
36894
+ screenshot_size_kb: +(result.size_bytes / 1024).toFixed(1)
36895
+ };
36896
+ if (opts.json) {
36897
+ console.log(JSON.stringify(summary, null, 2));
36898
+ } else {
36899
+ console.log(chalk.green(`\u2713 ${title}`));
36900
+ console.log(chalk.blue(` URL: ${currentUrl}`));
36901
+ console.log(chalk.gray(` Text: ${text.length} chars, Links: ${links.length}`));
36902
+ console.log(chalk.gray(` Screenshot: ${result.path} (${summary.screenshot_size_kb} KB)`));
36903
+ }
36904
+ await closeSession2(session.id);
36905
+ });
36906
+ program2.command("screenshot <url>").description("Navigate to a URL and take a screenshot").option("--engine <engine>", "Browser engine", "auto").option("--selector <selector>", "CSS selector for element screenshot").option("--full-page", "Capture full page").option("--format <format>", "Image format: png|jpeg|webp", "png").option("--headed", "Run in headed (visible) mode").action(async (url, opts) => {
36907
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
35455
36908
  await navigate(page, url);
35456
36909
  const result = await takeScreenshot(page, {
35457
36910
  selector: opts.selector,
@@ -35462,11 +36915,13 @@ program2.command("screenshot <url>").description("Navigate to a URL and take a s
35462
36915
  console.log(chalk.gray(` Size: ${(result.size_bytes / 1024).toFixed(1)} KB`));
35463
36916
  await closeSession2(session.id);
35464
36917
  });
35465
- program2.command("extract <url>").description("Extract content from a URL").option("--engine <engine>", "Browser engine", "auto").option("--selector <selector>", "CSS selector").option("--format <format>", "Format: text|html|links|table|structured", "text").action(async (url, opts) => {
35466
- const { session, page } = await createSession2({ engine: opts.engine, headless: true });
36918
+ program2.command("extract <url>").description("Extract content from a URL").option("--engine <engine>", "Browser engine", "auto").option("--selector <selector>", "CSS selector").option("--format <format>", "Format: text|html|links|table|structured", "text").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
36919
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
35467
36920
  await navigate(page, url);
35468
36921
  const result = await extract(page, { format: opts.format, selector: opts.selector });
35469
- if (opts.format === "links" && result.links) {
36922
+ if (opts.json) {
36923
+ console.log(JSON.stringify(result, null, 2));
36924
+ } else if (opts.format === "links" && result.links) {
35470
36925
  result.links.forEach((l) => console.log(l));
35471
36926
  } else if (opts.format === "table" && result.table) {
35472
36927
  result.table.forEach((row) => console.log(row.join("\t")));
@@ -35475,8 +36930,8 @@ program2.command("extract <url>").description("Extract content from a URL").opti
35475
36930
  }
35476
36931
  await closeSession2(session.id);
35477
36932
  });
35478
- program2.command("eval <url> <script>").description("Run JavaScript in a page context").option("--engine <engine>", "Browser engine", "auto").action(async (url, script, opts) => {
35479
- const { session, page } = await createSession2({ engine: opts.engine, headless: true });
36933
+ program2.command("eval <url> <script>").description("Run JavaScript in a page context").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").action(async (url, script, opts) => {
36934
+ const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
35480
36935
  await navigate(page, url);
35481
36936
  const result = await page.evaluate(script);
35482
36937
  console.log(JSON.stringify(result, null, 2));
@@ -35502,14 +36957,16 @@ ${result.errors.length} errors:`));
35502
36957
  }
35503
36958
  });
35504
36959
  var sessionCmd = program2.command("session").description("Manage browser sessions");
35505
- sessionCmd.command("create").description("Create a new browser session").option("--engine <engine>", "Browser engine", "auto").option("--url <url>", "Start URL").action(async (opts) => {
35506
- const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url });
36960
+ sessionCmd.command("create").description("Create a new browser session").option("--engine <engine>", "Browser engine", "auto").option("--url <url>", "Start URL").option("--headed", "Run in headed (visible) mode").action(async (opts) => {
36961
+ const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
35507
36962
  console.log(chalk.green(`\u2713 Session created`));
35508
36963
  console.log(JSON.stringify(session, null, 2));
35509
36964
  });
35510
- sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status").action((opts) => {
36965
+ sessionCmd.command("list").description("List all sessions").option("--status <status>", "Filter by status").option("--json", "Output as JSON").action((opts) => {
35511
36966
  const sessions = listSessions2(opts.status ? { status: opts.status } : undefined);
35512
- if (sessions.length === 0) {
36967
+ if (opts.json) {
36968
+ console.log(JSON.stringify(sessions, null, 2));
36969
+ } else if (sessions.length === 0) {
35513
36970
  console.log(chalk.gray("No sessions found"));
35514
36971
  } else {
35515
36972
  sessions.forEach((s) => console.log(`${s.id} [${s.status}] ${s.engine} ${s.start_url ?? ""}`));
@@ -35519,9 +36976,25 @@ sessionCmd.command("close <id>").description("Close a session").action(async (id
35519
36976
  await closeSession2(id);
35520
36977
  console.log(chalk.green(`\u2713 Session closed: ${id}`));
35521
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
+ });
35522
36995
  var recordCmd = program2.command("record").description("Manage action recordings");
35523
- recordCmd.command("start <name>").description("Start recording actions in a new session").option("--url <url>", "Start URL").option("--engine <engine>", "Browser engine", "auto").action(async (name, opts) => {
35524
- const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url });
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) => {
36997
+ const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
35525
36998
  const recording = startRecording(session.id, name, opts.url);
35526
36999
  console.log(chalk.green(`\u2713 Recording started`));
35527
37000
  console.log(` Recording ID: ${recording.id}`);
@@ -35532,8 +37005,8 @@ recordCmd.command("stop <recording_id>").description("Stop an active recording")
35532
37005
  console.log(chalk.green(`\u2713 Recording stopped: ${recording.name}`));
35533
37006
  console.log(` Steps: ${recording.steps.length}`);
35534
37007
  });
35535
- recordCmd.command("replay <recording_id>").description("Replay a recording in a new session").option("--url <url>", "Override start URL").option("--engine <engine>", "Browser engine", "auto").action(async (id, opts) => {
35536
- const { session, page } = await createSession2({ engine: opts.engine, startUrl: opts.url });
37008
+ recordCmd.command("replay <recording_id>").description("Replay a recording in a new session").option("--url <url>", "Override start URL").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").action(async (id, opts) => {
37009
+ const { session, page } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
35537
37010
  const result = await replayRecording(id, page);
35538
37011
  console.log(result.success ? chalk.green("\u2713 Replay complete") : chalk.red("\u2717 Replay had errors"));
35539
37012
  console.log(` Steps: ${result.steps_executed} executed, ${result.steps_failed} failed`);
@@ -35581,6 +37054,19 @@ projectCmd.command("list").description("List all projects").action(() => {
35581
37054
  projects.forEach((p) => console.log(`${p.id} "${p.name}" ${p.path}`));
35582
37055
  }
35583
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
+ });
35584
37070
  program2.command("install-browser").description("Install a browser engine").option("--engine <engine>", "Engine to install: lightpanda|chromium", "chromium").action(async (opts) => {
35585
37071
  if (opts.engine === "chromium") {
35586
37072
  const { execSync: execSync3 } = await import("child_process");
@@ -35672,11 +37158,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
35672
37158
  });
35673
37159
  galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
35674
37160
  const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
35675
- const { existsSync: existsSync9 } = await import("fs");
37161
+ const { existsSync: existsSync10 } = await import("fs");
35676
37162
  const entries = listEntries2({ limit: 9999 });
35677
37163
  let removed = 0;
35678
37164
  for (const e of entries) {
35679
- if (!existsSync9(e.path)) {
37165
+ if (!existsSync10(e.path)) {
35680
37166
  deleteEntry2(e.id);
35681
37167
  removed++;
35682
37168
  }