@hasna/browser 0.4.5 → 0.4.7

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
@@ -14372,6 +14372,340 @@ var init_bun_webview = __esm(() => {
14372
14372
 
14373
14373
  // src/engines/tui.ts
14374
14374
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
14375
+ function normalizeRowText(text) {
14376
+ return text.replace(/\u00a0/g, " ").replace(/\s+$/g, "");
14377
+ }
14378
+ function buildRowRefs(rows, method, totalRows, rowCount) {
14379
+ const refs = {};
14380
+ const firstVisibleRow = method === "buffer" ? Math.max(0, rowCount - totalRows) : 0;
14381
+ rows.forEach((text, index) => {
14382
+ refs[`@r${index}`] = {
14383
+ row: index,
14384
+ text,
14385
+ visible: method === "dom" ? true : index >= firstVisibleRow,
14386
+ selector: method === "dom" ? `#takumi-tui-dom-root [data-row="${index}"]` : undefined
14387
+ };
14388
+ });
14389
+ return refs;
14390
+ }
14391
+ async function configureDomRenderer(page, options) {
14392
+ await page.evaluate((opts) => {
14393
+ const runtimeKey = "__takumiTuiDomRenderer";
14394
+ const rootId = "takumi-tui-dom-root";
14395
+ const styleId = "takumi-tui-dom-style";
14396
+ const win = window;
14397
+ const ensureStyle = () => {
14398
+ let style = document.getElementById(styleId);
14399
+ if (!style) {
14400
+ style = document.createElement("style");
14401
+ style.id = styleId;
14402
+ document.head.appendChild(style);
14403
+ }
14404
+ style.textContent = `
14405
+ #${rootId} {
14406
+ position: absolute;
14407
+ inset: 0;
14408
+ overflow: hidden;
14409
+ display: flex;
14410
+ flex-direction: column;
14411
+ white-space: pre;
14412
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
14413
+ line-height: 1.2;
14414
+ background: var(--takumi-tui-bg, #1e1e1e);
14415
+ color: var(--takumi-tui-fg, #d4d4d4);
14416
+ z-index: 4;
14417
+ pointer-events: none;
14418
+ user-select: text;
14419
+ }
14420
+ #${rootId}[data-active="0"] {
14421
+ display: none;
14422
+ }
14423
+ #${rootId} .takumi-tui-dom-row {
14424
+ display: flex;
14425
+ min-height: 1.2em;
14426
+ }
14427
+ #${rootId} .takumi-tui-dom-cell {
14428
+ display: inline-flex;
14429
+ align-items: center;
14430
+ justify-content: center;
14431
+ min-width: 0.62em;
14432
+ height: 1.2em;
14433
+ }
14434
+ #${rootId} .takumi-tui-dom-cell[data-cursor="true"] {
14435
+ outline: 1px solid currentColor;
14436
+ outline-offset: -1px;
14437
+ }
14438
+ body[data-takumi-dom-render="1"] .xterm-rows,
14439
+ body[data-takumi-dom-render="1"] .xterm-text-layer,
14440
+ body[data-takumi-dom-render="1"] .xterm-cursor-layer,
14441
+ body[data-takumi-dom-render="1"] .xterm-selection-layer {
14442
+ opacity: 0 !important;
14443
+ }
14444
+ `;
14445
+ };
14446
+ const ensureRoot = () => {
14447
+ let root = document.getElementById(rootId);
14448
+ if (!root) {
14449
+ root = document.createElement("div");
14450
+ root.id = rootId;
14451
+ root.setAttribute("role", "grid");
14452
+ root.setAttribute("aria-label", "Terminal DOM renderer");
14453
+ const host = document.getElementById("terminal-container") ?? document.querySelector(".xterm") ?? document.body;
14454
+ if (getComputedStyle(host).position === "static") {
14455
+ host.style.position = "relative";
14456
+ }
14457
+ host.appendChild(root);
14458
+ }
14459
+ root.style.setProperty("--takumi-tui-bg", opts.theme === "light" ? "#ffffff" : "#1e1e1e");
14460
+ root.style.setProperty("--takumi-tui-fg", opts.theme === "light" ? "#1e1e1e" : "#d4d4d4");
14461
+ root.style.fontSize = `${opts.fontSize ?? 14}px`;
14462
+ root.dataset.active = opts.active ? "1" : "0";
14463
+ if (opts.active)
14464
+ root.removeAttribute("aria-hidden");
14465
+ else
14466
+ root.setAttribute("aria-hidden", "true");
14467
+ document.body.dataset.takumiDomRender = opts.active ? "1" : "0";
14468
+ return root;
14469
+ };
14470
+ const readCellChars = (line, col) => {
14471
+ try {
14472
+ const cell = typeof line?.getCell === "function" ? line.getCell(col) : null;
14473
+ const chars = typeof cell?.getChars === "function" ? cell.getChars() : "";
14474
+ if (chars)
14475
+ return chars;
14476
+ } catch {}
14477
+ try {
14478
+ const text = typeof line?.translateToString === "function" ? line.translateToString(false, col, col + 1) : "";
14479
+ if (text)
14480
+ return text;
14481
+ } catch {}
14482
+ return " ";
14483
+ };
14484
+ const buildState = (activeOnly) => {
14485
+ const term = win.term ?? win.terminal;
14486
+ if (!term?.buffer?.active) {
14487
+ return {
14488
+ text: "",
14489
+ rows: [],
14490
+ row_count: 0,
14491
+ cols: null,
14492
+ total_rows: 0,
14493
+ buffer_length: null,
14494
+ cursor_row: -1,
14495
+ cursor_col: -1,
14496
+ font_size: null,
14497
+ theme: opts.theme
14498
+ };
14499
+ }
14500
+ const buf = term.buffer.active;
14501
+ const rows = [];
14502
+ const root = ensureRoot();
14503
+ const fragment = document.createDocumentFragment();
14504
+ for (let row = 0;row < buf.length; row++) {
14505
+ const line = buf.getLine(row);
14506
+ if (!line)
14507
+ continue;
14508
+ const rowEl = document.createElement("div");
14509
+ rowEl.className = "takumi-tui-dom-row";
14510
+ rowEl.setAttribute("role", "row");
14511
+ rowEl.dataset.row = String(row);
14512
+ rowEl.setAttribute("aria-rowindex", String(row + 1));
14513
+ let rowText = "";
14514
+ for (let col = 0;col < term.cols; col++) {
14515
+ const char = readCellChars(line, col) || " ";
14516
+ rowText += char;
14517
+ const cellEl = document.createElement("span");
14518
+ cellEl.className = "takumi-tui-dom-cell";
14519
+ cellEl.setAttribute("role", "gridcell");
14520
+ cellEl.dataset.row = String(row);
14521
+ cellEl.dataset.col = String(col);
14522
+ cellEl.setAttribute("aria-colindex", String(col + 1));
14523
+ cellEl.textContent = char;
14524
+ if (buf.cursorY === row && buf.cursorX === col) {
14525
+ cellEl.dataset.cursor = "true";
14526
+ }
14527
+ rowEl.appendChild(cellEl);
14528
+ }
14529
+ rows.push(rowText.replace(/\s+$/g, ""));
14530
+ rowEl.setAttribute("aria-label", rows[rows.length - 1] || " ");
14531
+ fragment.appendChild(rowEl);
14532
+ }
14533
+ root.replaceChildren(fragment);
14534
+ root.setAttribute("aria-rowcount", String(rows.length));
14535
+ root.dataset.method = "dom";
14536
+ return {
14537
+ text: rows.join(`
14538
+ `).trimEnd(),
14539
+ rows,
14540
+ row_count: rows.length,
14541
+ cols: term.cols,
14542
+ total_rows: term.rows,
14543
+ buffer_length: buf.length,
14544
+ cursor_row: buf.cursorY,
14545
+ cursor_col: buf.cursorX,
14546
+ font_size: term.options?.fontSize ?? null,
14547
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
14548
+ };
14549
+ };
14550
+ ensureStyle();
14551
+ ensureRoot();
14552
+ if (!win[runtimeKey]) {
14553
+ win[runtimeKey] = {
14554
+ sync: () => buildState(false),
14555
+ activate: (active) => {
14556
+ const root = ensureRoot();
14557
+ root.dataset.active = active ? "1" : "0";
14558
+ if (active)
14559
+ root.removeAttribute("aria-hidden");
14560
+ else
14561
+ root.setAttribute("aria-hidden", "true");
14562
+ document.body.dataset.takumiDomRender = active ? "1" : "0";
14563
+ }
14564
+ };
14565
+ const intervalId = window.setInterval(() => {
14566
+ try {
14567
+ win[runtimeKey]?.sync?.();
14568
+ } catch {}
14569
+ }, 50);
14570
+ win[runtimeKey].intervalId = intervalId;
14571
+ }
14572
+ win[runtimeKey].activate(opts.active);
14573
+ win[runtimeKey].sync();
14574
+ }, options);
14575
+ }
14576
+ async function destroyDomRenderer(page) {
14577
+ await page.evaluate(() => {
14578
+ const runtimeKey = "__takumiTuiDomRenderer";
14579
+ const win = window;
14580
+ if (win[runtimeKey]?.intervalId) {
14581
+ clearInterval(win[runtimeKey].intervalId);
14582
+ }
14583
+ delete win[runtimeKey];
14584
+ document.getElementById("takumi-tui-dom-root")?.remove();
14585
+ document.getElementById("takumi-tui-dom-style")?.remove();
14586
+ delete document.body.dataset.takumiDomRender;
14587
+ }).catch(() => {});
14588
+ }
14589
+ async function readDomMirrorState(page) {
14590
+ return page.evaluate(() => {
14591
+ const runtime = window.__takumiTuiDomRenderer;
14592
+ if (runtime?.sync)
14593
+ return runtime.sync();
14594
+ const rowEls = Array.from(document.querySelectorAll("#takumi-tui-dom-root [data-row]"));
14595
+ const rows = rowEls.map((row) => row.getAttribute("aria-label") ?? row.textContent ?? "");
14596
+ const term = window.term ?? window.terminal;
14597
+ const active = term?.buffer?.active;
14598
+ return {
14599
+ text: rows.join(`
14600
+ `).trimEnd(),
14601
+ rows,
14602
+ row_count: rows.length,
14603
+ cols: term?.cols ?? null,
14604
+ total_rows: term?.rows ?? rows.length,
14605
+ buffer_length: active?.length ?? rows.length,
14606
+ cursor_row: active?.cursorY ?? -1,
14607
+ cursor_col: active?.cursorX ?? -1,
14608
+ font_size: term?.options?.fontSize ?? null,
14609
+ theme: term?.options?.theme?.background === "#ffffff" ? "light" : "dark"
14610
+ };
14611
+ });
14612
+ }
14613
+ function isDomMethod(method) {
14614
+ return method === "dom";
14615
+ }
14616
+ async function withTimeout(label, operation, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
14617
+ let timedOut = false;
14618
+ const timer = setTimeout(() => {
14619
+ timedOut = true;
14620
+ }, timeoutMs);
14621
+ try {
14622
+ return await operation();
14623
+ } catch (err) {
14624
+ if (timedOut) {
14625
+ throw new BrowserError(`${label} timed out after ${timeoutMs}ms \u2014 ttyd/playwright connection may be unhealthy. Try closing and re-opening the session.`, "TUI_TIMEOUT");
14626
+ }
14627
+ throw err;
14628
+ } finally {
14629
+ clearTimeout(timer);
14630
+ }
14631
+ }
14632
+ async function isTuiHealthy(session) {
14633
+ const start = Date.now();
14634
+ try {
14635
+ await Promise.race([
14636
+ session.page.evaluate(() => {
14637
+ const term = window.term ?? window.terminal;
14638
+ if (!term)
14639
+ return false;
14640
+ if (!term.buffer?.active)
14641
+ return false;
14642
+ return true;
14643
+ }),
14644
+ new Promise((_, reject) => setTimeout(() => reject(new Error("health check timeout")), HEALTH_CHECK_TIMEOUT_MS))
14645
+ ]);
14646
+ const latency = Date.now() - start;
14647
+ return { healthy: true, latency_ms: latency };
14648
+ } catch (err) {
14649
+ return { healthy: false, reason: err?.message ?? "unreachable" };
14650
+ }
14651
+ }
14652
+ async function reconnectTui(session, command, options = {}) {
14653
+ const port = session.port;
14654
+ try {
14655
+ session.ttydProcess.kill("SIGTERM");
14656
+ } catch {}
14657
+ try {
14658
+ await session.page.close();
14659
+ } catch {}
14660
+ try {
14661
+ await session.browser.close();
14662
+ } catch {}
14663
+ const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
14664
+ ttydProcess.on("error", (err) => {
14665
+ console.error(`[tui] reconnect ttyd error: ${err.message}`);
14666
+ });
14667
+ await waitForTtyd(port);
14668
+ const viewport = options.viewport ?? { width: 1280, height: 720 };
14669
+ const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
14670
+ const page = await getPage(browser, { viewport });
14671
+ await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
14672
+ await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
14673
+ let resolvedTheme = "dark";
14674
+ const req = options.theme ?? "dark";
14675
+ if (req === "light" || req === "dark") {
14676
+ resolvedTheme = req;
14677
+ } else {
14678
+ try {
14679
+ const r = execSync2("defaults read -g AppleInterfaceStyle 2>/dev/null", { encoding: "utf8" }).trim();
14680
+ resolvedTheme = r === "Dark" ? "dark" : "light";
14681
+ } catch {
14682
+ resolvedTheme = "light";
14683
+ }
14684
+ }
14685
+ const themeColors = THEMES[resolvedTheme];
14686
+ await page.evaluate((theme) => {
14687
+ const term = window.term ?? window.terminal;
14688
+ if (term?.options)
14689
+ term.options.theme = theme;
14690
+ document.body.style.backgroundColor = theme.background;
14691
+ }, themeColors);
14692
+ const method = options.method ?? session.method;
14693
+ await configureDomRenderer(page, {
14694
+ active: isDomMethod(method),
14695
+ theme: resolvedTheme,
14696
+ fontSize: options.fontSize
14697
+ });
14698
+ return {
14699
+ ttydProcess,
14700
+ port,
14701
+ browser,
14702
+ page,
14703
+ theme: resolvedTheme,
14704
+ method,
14705
+ lastHealthCheck: Date.now(),
14706
+ reconnectCount: session.reconnectCount + 1
14707
+ };
14708
+ }
14375
14709
  function isTuiAvailable() {
14376
14710
  try {
14377
14711
  execSync2("which ttyd", { stdio: "ignore" });
@@ -14384,7 +14718,7 @@ async function findAvailablePort(startPort) {
14384
14718
  let port = startPort;
14385
14719
  for (let i = 0;i < 100; i++) {
14386
14720
  try {
14387
- const resp = await fetch(`http://localhost:${port}`);
14721
+ await fetch(`http://localhost:${port}`);
14388
14722
  port++;
14389
14723
  } catch {
14390
14724
  return port;
@@ -14410,24 +14744,16 @@ async function launchTui(command, options = {}) {
14410
14744
  }
14411
14745
  const port = await findAvailablePort(nextPort);
14412
14746
  nextPort = port + 1;
14413
- const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], {
14414
- stdio: "ignore",
14415
- detached: false
14416
- });
14747
+ const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
14417
14748
  ttydProcess.on("error", (err) => {
14418
14749
  console.error(`[tui] ttyd process error: ${err.message}`);
14419
14750
  });
14420
14751
  try {
14421
14752
  await waitForTtyd(port);
14422
14753
  const viewport = options.viewport ?? { width: 1280, height: 720 };
14423
- const browser = await launchPlaywright({
14424
- headless: options.headless ?? true,
14425
- viewport
14426
- });
14754
+ const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
14427
14755
  const page = await getPage(browser, { viewport });
14428
- await page.goto(`http://localhost:${port}`, {
14429
- waitUntil: "domcontentloaded"
14430
- });
14756
+ await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
14431
14757
  await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
14432
14758
  let resolvedTheme = "dark";
14433
14759
  const requestedTheme = options.theme ?? "system";
@@ -14446,16 +14772,15 @@ async function launchTui(command, options = {}) {
14446
14772
  const themeColors = THEMES[resolvedTheme];
14447
14773
  await page.evaluate((theme) => {
14448
14774
  const term = window.term ?? window.terminal;
14449
- if (term?.options) {
14775
+ if (term?.options)
14450
14776
  term.options.theme = theme;
14451
- }
14452
14777
  document.body.style.backgroundColor = theme.background;
14453
14778
  const container = document.getElementById("terminal-container");
14454
14779
  if (container)
14455
14780
  container.style.backgroundColor = theme.background;
14456
- const viewport2 = document.querySelector(".xterm-viewport");
14457
- if (viewport2)
14458
- viewport2.style.backgroundColor = theme.background;
14781
+ const vp = document.querySelector(".xterm-viewport");
14782
+ if (vp)
14783
+ vp.style.backgroundColor = theme.background;
14459
14784
  }, themeColors);
14460
14785
  if (options.fontSize) {
14461
14786
  await page.evaluate((size) => {
@@ -14464,13 +14789,115 @@ async function launchTui(command, options = {}) {
14464
14789
  term.options.fontSize = size;
14465
14790
  }, options.fontSize);
14466
14791
  }
14467
- return { ttydProcess, port, browser, page, theme: resolvedTheme };
14792
+ const method = options.method ?? "buffer";
14793
+ await configureDomRenderer(page, {
14794
+ active: isDomMethod(method),
14795
+ theme: resolvedTheme,
14796
+ fontSize: options.fontSize
14797
+ });
14798
+ return {
14799
+ ttydProcess,
14800
+ port,
14801
+ browser,
14802
+ page,
14803
+ theme: resolvedTheme,
14804
+ method,
14805
+ lastHealthCheck: Date.now(),
14806
+ reconnectCount: 0
14807
+ };
14468
14808
  } catch (err) {
14469
- ttydProcess.kill();
14809
+ try {
14810
+ ttydProcess.kill("SIGTERM");
14811
+ } catch {}
14470
14812
  throw err;
14471
14813
  }
14472
14814
  }
14815
+ async function getBufferState(page) {
14816
+ return page.evaluate(() => {
14817
+ const term = window.term ?? window.terminal;
14818
+ if (!term?.buffer?.active) {
14819
+ return {
14820
+ text: "",
14821
+ rows: [],
14822
+ row_count: 0,
14823
+ cols: null,
14824
+ total_rows: 0,
14825
+ buffer_length: null,
14826
+ cursor_row: -1,
14827
+ cursor_col: -1,
14828
+ font_size: null,
14829
+ theme: "dark"
14830
+ };
14831
+ }
14832
+ const buf = term.buffer.active;
14833
+ const rows = [];
14834
+ for (let i = 0;i < buf.length; i++) {
14835
+ const line = buf.getLine(i);
14836
+ if (line)
14837
+ rows.push(line.translateToString(true));
14838
+ }
14839
+ return {
14840
+ text: rows.join(`
14841
+ `).trimEnd(),
14842
+ rows,
14843
+ row_count: buf.length,
14844
+ cols: term.cols,
14845
+ total_rows: term.rows,
14846
+ buffer_length: buf.length,
14847
+ cursor_row: buf.cursorY,
14848
+ cursor_col: buf.cursorX,
14849
+ font_size: term.options?.fontSize ?? null,
14850
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
14851
+ };
14852
+ });
14853
+ }
14854
+ async function getDomState(page) {
14855
+ return readDomMirrorState(page);
14856
+ }
14857
+ async function getTerminalState(page, method = "buffer", timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
14858
+ return withTimeout("getTerminalState", async () => {
14859
+ const raw = method === "dom" ? await getDomState(page) : await getBufferState(page);
14860
+ const rows = raw.rows.map(normalizeRowText);
14861
+ const text = rows.join(`
14862
+ `).trimEnd();
14863
+ return {
14864
+ ...raw,
14865
+ method,
14866
+ rows,
14867
+ text,
14868
+ refs: buildRowRefs(rows, method, raw.total_rows, raw.row_count)
14869
+ };
14870
+ }, timeoutMs);
14871
+ }
14872
+ async function getTerminalText(page, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS, method = "buffer") {
14873
+ const state = await getTerminalState(page, method, timeoutMs);
14874
+ return state.text;
14875
+ }
14876
+ async function waitForTerminalText(page, text, timeoutMs = 30000, method = "buffer") {
14877
+ const start = Date.now();
14878
+ while (Date.now() - start < timeoutMs) {
14879
+ let healthy = false;
14880
+ try {
14881
+ await Promise.race([
14882
+ page.evaluate(() => {
14883
+ const term = window.term ?? window.terminal;
14884
+ return term?.buffer?.active ? true : false;
14885
+ }),
14886
+ new Promise((_, reject) => setTimeout(() => reject(new Error("probe timeout")), 2000))
14887
+ ]);
14888
+ healthy = true;
14889
+ } catch {}
14890
+ if (!healthy)
14891
+ return { found: false, elapsed_ms: Date.now() - start, stuck: true };
14892
+ const content = await getTerminalText(page, DEFAULT_TOOL_TIMEOUT_MS, method);
14893
+ if (content.includes(text))
14894
+ return { found: true, elapsed_ms: Date.now() - start, stuck: false };
14895
+ await new Promise((r) => setTimeout(r, 250));
14896
+ }
14897
+ return { found: false, elapsed_ms: timeoutMs, stuck: false };
14898
+ }
14473
14899
  async function closeTui(session) {
14900
+ await destroyDomRenderer(session.page);
14474
14901
  try {
14475
14902
  await session.page.close();
14476
14903
  } catch {}
@@ -14480,8 +14907,11 @@ async function closeTui(session) {
14480
14907
  try {
14481
14908
  session.ttydProcess.kill("SIGTERM");
14482
14909
  } catch {}
14910
+ try {
14911
+ session.ttydProcess.kill("SIGKILL");
14912
+ } catch {}
14483
14913
  }
14484
- var DEFAULT_TTYD_PORT_START = 7780, nextPort, THEMES;
14914
+ var DEFAULT_TTYD_PORT_START = 7780, nextPort, DEFAULT_TOOL_TIMEOUT_MS = 15000, HEALTH_CHECK_TIMEOUT_MS = 3000, THEMES;
14485
14915
  var init_tui = __esm(() => {
14486
14916
  init_types2();
14487
14917
  init_playwright();
@@ -14811,19 +15241,13 @@ Object.defineProperty(navigator, 'plugins', {
14811
15241
  { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
14812
15242
  { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
14813
15243
  ];
14814
- // Mimic PluginArray interface
14815
- const pluginArray = Object.create(PluginArray.prototype);
15244
+ // Mimic PluginArray interface \u2014 guard against removed prototypes
15245
+ const pluginArray = {};
14816
15246
  plugins.forEach((p, i) => {
14817
- const plugin = Object.create(Plugin.prototype);
14818
- Object.defineProperties(plugin, {
14819
- name: { value: p.name, enumerable: true },
14820
- filename: { value: p.filename, enumerable: true },
14821
- description: { value: p.description, enumerable: true },
14822
- length: { value: p.length, enumerable: true },
14823
- });
15247
+ const plugin = { ...p, item: () => null };
14824
15248
  pluginArray[i] = plugin;
14825
15249
  });
14826
- Object.defineProperty(pluginArray, 'length', { value: plugins.length });
15250
+ pluginArray.length = plugins.length;
14827
15251
  pluginArray.item = (i) => pluginArray[i] || null;
14828
15252
  pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
14829
15253
  pluginArray.refresh = () => {};
@@ -15768,6 +16192,7 @@ function watchPage(page, opts) {
15768
16192
  const changes = [];
15769
16193
  const intervalMs = opts?.intervalMs ?? 500;
15770
16194
  const maxChanges = opts?.maxChanges ?? 50;
16195
+ const sessionId = opts?.sessionId;
15771
16196
  const interval = setInterval(async () => {
15772
16197
  if (changes.length >= maxChanges)
15773
16198
  return;
@@ -15781,7 +16206,7 @@ function watchPage(page, opts) {
15781
16206
  }
15782
16207
  } catch {}
15783
16208
  }, intervalMs);
15784
- activeWatches.set(id, { interval, changes });
16209
+ activeWatches.set(id, { interval, changes, sessionId });
15785
16210
  return {
15786
16211
  id,
15787
16212
  stop: () => {
@@ -15800,10 +16225,12 @@ function stopWatch(watchId) {
15800
16225
  activeWatches.delete(watchId);
15801
16226
  }
15802
16227
  }
15803
- function stopAllWatchesForSession(_sessionId) {
15804
- for (const [id, w] of activeWatches) {
15805
- clearInterval(w.interval);
15806
- activeWatches.delete(id);
16228
+ function stopAllWatchesForSession(sessionId) {
16229
+ for (const [id, w] of [...activeWatches]) {
16230
+ if (!sessionId || w.sessionId === sessionId) {
16231
+ clearInterval(w.interval);
16232
+ activeWatches.delete(id);
16233
+ }
15807
16234
  }
15808
16235
  }
15809
16236
  async function clickRef(page, sessionId, ref, opts) {
@@ -15884,6 +16311,7 @@ var init_actions = __esm(() => {
15884
16311
  // src/lib/session.ts
15885
16312
  var exports_session = {};
15886
16313
  __export(exports_session, {
16314
+ setSessionTui: () => setSessionTui,
15887
16315
  setSessionPage: () => setSessionPage,
15888
16316
  renameSession: () => renameSession2,
15889
16317
  listSessions: () => listSessions2,
@@ -15891,8 +16319,10 @@ __export(exports_session, {
15891
16319
  isAutoGallery: () => isAutoGallery,
15892
16320
  hasActiveHandle: () => hasActiveHandle,
15893
16321
  getTokenBudget: () => getTokenBudget,
16322
+ getSessionTuiSession: () => getSessionTuiSession,
15894
16323
  getSessionPage: () => getSessionPage,
15895
16324
  getSessionEngine: () => getSessionEngine,
16325
+ getSessionCommand: () => getSessionCommand,
15896
16326
  getSessionByName: () => getSessionByName2,
15897
16327
  getSessionBunView: () => getSessionBunView,
15898
16328
  getSessionBrowser: () => getSessionBrowser,
@@ -15938,7 +16368,7 @@ async function createSession2(opts = {}) {
15938
16368
  try {
15939
16369
  cleanups2.push(setupDialogHandler(page2, session2.id));
15940
16370
  } catch {}
15941
- handles.set(session2.id, { browser: cdpBrowser, bunView: null, tuiSession: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
16371
+ handles.set(session2.id, { browser: cdpBrowser, bunView: null, tuiSession: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
15942
16372
  return { session: session2, page: page2 };
15943
16373
  }
15944
16374
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
@@ -15952,13 +16382,29 @@ async function createSession2(opts = {}) {
15952
16382
  browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
15953
16383
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
15954
16384
  } else {
15955
- bunView = new BunWebViewSession({
16385
+ const testView = new BunWebViewSession({
15956
16386
  width: opts.viewport?.width ?? 1280,
15957
16387
  height: opts.viewport?.height ?? 720,
15958
16388
  profile: opts.name ?? undefined
15959
16389
  });
15960
- if (opts.stealth) {}
15961
- page = createBunProxy(bunView);
16390
+ let bunWorks = true;
16391
+ try {
16392
+ await testView.goto("data:text/html,<html></html>");
16393
+ } catch {
16394
+ bunWorks = false;
16395
+ try {
16396
+ await testView.close();
16397
+ } catch {}
16398
+ }
16399
+ if (!bunWorks) {
16400
+ console.warn("[browser] Bun.WebView exists but Chrome not available \u2014 falling back to playwright");
16401
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
16402
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
16403
+ } else {
16404
+ bunView = testView;
16405
+ if (opts.stealth) {}
16406
+ page = createBunProxy(bunView);
16407
+ }
15962
16408
  }
15963
16409
  } else if (resolvedEngine === "lightpanda") {
15964
16410
  browser = await connectLightpanda();
@@ -15970,7 +16416,8 @@ async function createSession2(opts = {}) {
15970
16416
  headless: opts.headless ?? true,
15971
16417
  viewport: opts.viewport,
15972
16418
  theme: opts.tuiTheme ?? "system",
15973
- fontSize: opts.tuiFontSize
16419
+ fontSize: opts.tuiFontSize,
16420
+ method: opts.tuiMethod ?? "buffer"
15974
16421
  });
15975
16422
  browser = tuiSess.browser;
15976
16423
  page = tuiSess.page;
@@ -15996,7 +16443,7 @@ async function createSession2(opts = {}) {
15996
16443
  try {
15997
16444
  cleanups2.push(setupDialogHandler(page, session2.id));
15998
16445
  } catch {}
15999
- handles.set(session2.id, { browser, bunView: null, tuiSession: tuiSess, page, engine: "tui", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
16446
+ handles.set(session2.id, { browser, bunView: null, tuiSession: tuiSess, page, engine: "tui", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "bash" });
16000
16447
  return { session: session2, page };
16001
16448
  } else {
16002
16449
  browser = await pool.acquire(opts.headless ?? true);
@@ -16068,7 +16515,7 @@ async function createSession2(opts = {}) {
16068
16515
  } catch {}
16069
16516
  }
16070
16517
  }
16071
- handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
16518
+ handles.set(session.id, { browser, bunView, tuiSession: null, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false, startUrl: opts.startUrl ?? "" });
16072
16519
  if (opts.startUrl) {
16073
16520
  try {
16074
16521
  if (bunView) {
@@ -16120,6 +16567,23 @@ function getSessionEngine(sessionId) {
16120
16567
  function hasActiveHandle(sessionId) {
16121
16568
  return handles.has(sessionId);
16122
16569
  }
16570
+ function getSessionTuiSession(sessionId) {
16571
+ return handles.get(sessionId)?.tuiSession ?? null;
16572
+ }
16573
+ function setSessionTui(sessionId, tuiSess) {
16574
+ const handle = handles.get(sessionId);
16575
+ if (!handle)
16576
+ throw new SessionNotFoundError(sessionId);
16577
+ handle.tuiSession = tuiSess;
16578
+ handle.page = tuiSess.page;
16579
+ if (tuiSess.browser !== handle.browser) {
16580
+ handle.browser = tuiSess.browser;
16581
+ }
16582
+ handle.lastActivity = Date.now();
16583
+ }
16584
+ function getSessionCommand(sessionId) {
16585
+ return handles.get(sessionId)?.startUrl ?? "bash";
16586
+ }
16123
16587
  function setSessionPage(sessionId, page) {
16124
16588
  const handle = handles.get(sessionId);
16125
16589
  if (!handle)
@@ -16128,38 +16592,43 @@ function setSessionPage(sessionId, page) {
16128
16592
  }
16129
16593
  async function closeSession2(sessionId) {
16130
16594
  const handle = handles.get(sessionId);
16131
- if (handle) {
16132
- for (const cleanup of handle.cleanups) {
16133
- try {
16134
- cleanup();
16135
- } catch {}
16136
- }
16137
- if (handle.bunView) {
16138
- try {
16139
- await handle.bunView.close();
16140
- } catch {}
16141
- } else if (handle.tuiSession) {} else {
16142
- try {
16143
- await handle.page.context().close();
16144
- } catch {}
16145
- if (handle.browser)
16146
- pool.release(handle.browser);
16595
+ try {
16596
+ if (handle) {
16597
+ for (const cleanup of handle.cleanups) {
16598
+ try {
16599
+ cleanup();
16600
+ } catch {}
16601
+ }
16602
+ if (handle.bunView) {
16603
+ try {
16604
+ await handle.bunView.close();
16605
+ } catch {}
16606
+ } else if (handle.tuiSession) {} else {
16607
+ try {
16608
+ await handle.page.context().close();
16609
+ } catch {}
16610
+ try {
16611
+ if (handle.browser)
16612
+ pool.release(handle.browser);
16613
+ } catch {}
16614
+ }
16147
16615
  }
16616
+ try {
16617
+ const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
16618
+ clearLastSnapshot2(sessionId);
16619
+ clearSessionRefs2(sessionId);
16620
+ } catch {}
16621
+ try {
16622
+ const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
16623
+ stopAllWatchesForSession2(sessionId);
16624
+ } catch {}
16625
+ try {
16626
+ const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
16627
+ clearDialogs2(sessionId);
16628
+ } catch {}
16629
+ } finally {
16148
16630
  handles.delete(sessionId);
16149
16631
  }
16150
- try {
16151
- const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
16152
- clearLastSnapshot2(sessionId);
16153
- clearSessionRefs2(sessionId);
16154
- } catch {}
16155
- try {
16156
- const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
16157
- stopAllWatchesForSession2(sessionId);
16158
- } catch {}
16159
- try {
16160
- const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
16161
- clearDialogs2(sessionId);
16162
- } catch {}
16163
16632
  return closeSession(sessionId);
16164
16633
  }
16165
16634
  function getSession2(sessionId) {
@@ -16260,14 +16729,16 @@ var init_session = __esm(() => {
16260
16729
  ttlInterval.unref();
16261
16730
  DB_PRUNE_INTERVAL_MS = 30 * 60000;
16262
16731
  dbPruneInterval = setInterval(() => {
16263
- try {
16264
- const { getDatabase: getDatabase2 } = (init_schema(), __toCommonJS(exports_schema));
16265
- const db = getDatabase2();
16266
- const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
16267
- db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16268
- db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16269
- db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16270
- } catch {}
16732
+ (async () => {
16733
+ try {
16734
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
16735
+ const db = getDatabase2();
16736
+ const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
16737
+ db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16738
+ db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16739
+ db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
16740
+ } catch {}
16741
+ })();
16271
16742
  }, DB_PRUNE_INTERVAL_MS);
16272
16743
  if (dbPruneInterval.unref)
16273
16744
  dbPruneInterval.unref();
@@ -22864,6 +23335,12 @@ __export(exports_gallery, {
22864
23335
  createEntry: () => createEntry
22865
23336
  });
22866
23337
  import { randomUUID as randomUUID4 } from "crypto";
23338
+ function validateDataPath(filePath) {
23339
+ if (filePath.includes("..")) {
23340
+ throw new Error(`File path must not contain '..': ${filePath}`);
23341
+ }
23342
+ return filePath;
23343
+ }
22867
23344
  function deserialize(row) {
22868
23345
  return {
22869
23346
  id: row.id,
@@ -22888,6 +23365,9 @@ function deserialize(row) {
22888
23365
  function createEntry(data) {
22889
23366
  const db = getDatabase();
22890
23367
  const id = randomUUID4();
23368
+ validateDataPath(data.path);
23369
+ if (data.thumbnail_path)
23370
+ validateDataPath(data.thumbnail_path);
22891
23371
  db.prepare(`
22892
23372
  INSERT INTO gallery_entries
22893
23373
  (id, session_id, project_id, url, title, path, thumbnail_path, format,
@@ -24214,11 +24694,21 @@ async function execBrowser(cfg, step, page, vars) {
24214
24694
  break;
24215
24695
  }
24216
24696
  }
24697
+ function isValidArg(arg) {
24698
+ return /^[a-zA-Z0-9._\-/@:]+$/.test(arg);
24699
+ }
24217
24700
  async function execConnector(cfg, step, vars) {
24218
24701
  const connector = cfg.connector;
24219
24702
  if (!connector)
24220
24703
  throw new Error("Connector step missing 'connector' in config");
24221
- const args = cfg.args ?? [];
24704
+ if (!ALLOWED_CONNECTORS.has(connector)) {
24705
+ throw new Error(`Unknown connector '${connector}'. Allowed: ${[...ALLOWED_CONNECTORS].join(", ")}`);
24706
+ }
24707
+ const args = (cfg.args ?? []).filter((a) => typeof a === "string").filter((a) => {
24708
+ if (!isValidArg(a))
24709
+ throw new Error(`Connector arg '${a}' contains disallowed characters`);
24710
+ return true;
24711
+ });
24222
24712
  const proc = Bun.spawn([`connect-${connector}`, ...args], {
24223
24713
  stdout: "pipe",
24224
24714
  stderr: "pipe",
@@ -24296,9 +24786,11 @@ async function aiSelfHeal(page, description, step) {
24296
24786
  function decodeHtmlEntities(str) {
24297
24787
  return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'");
24298
24788
  }
24789
+ var ALLOWED_CONNECTORS;
24299
24790
  var init_script_engine = __esm(() => {
24300
24791
  init_scripts();
24301
24792
  init_ai_inference();
24793
+ ALLOWED_CONNECTORS = new Set(["github", "linear", "slack", "jira", "notion", "gitlab"]);
24302
24794
  });
24303
24795
 
24304
24796
  // src/db/agents.ts
@@ -24857,11 +25349,14 @@ import { join as join14 } from "path";
24857
25349
  import { homedir as homedir8 } from "os";
24858
25350
  async function getCredentials(service) {
24859
25351
  try {
24860
- const { getSecret } = await import(`${homedir8()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
24861
- const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
24862
- const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
24863
- if (email?.value && password?.value) {
24864
- return { email: email.value, password: password.value };
25352
+ const secretsVaultPath = process.env["BROWSER_SECRETS_VAULT_PATH"];
25353
+ if (secretsVaultPath) {
25354
+ const { getSecret } = await import(secretsVaultPath);
25355
+ const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
25356
+ const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
25357
+ if (email?.value && password?.value) {
25358
+ return { email: email.value, password: password.value };
25359
+ }
24865
25360
  }
24866
25361
  } catch {}
24867
25362
  const secretsPath = join14(homedir8(), ".secrets");
@@ -29417,7 +29912,7 @@ ENGINES:
29417
29912
  - "cdp": Chrome DevTools Protocol \u2014 network monitoring, perf profiling, script injection
29418
29913
  - "lightpanda": fast headless for static pages
29419
29914
  - "bun": native Bun.WebView \u2014 fastest for screenshots and scraping
29420
- - "tui": terminal UI testing \u2014 launches a CLI/TUI app (Ink, Blessed, Bubbletea, etc.) via ttyd and connects Playwright to it. Pass the shell command as start_url (e.g. "htop", "bun run app.tsx"). All browser tools (screenshot, click, type, wait) work on the terminal. Use tui_theme to control dark/light appearance.
29915
+ - "tui": terminal UI testing \u2014 launches a CLI/TUI app (Ink, Blessed, Bubbletea, etc.) via ttyd and connects Playwright to it. Pass the shell command as start_url (e.g. "htop", "bun run app.tsx"). All browser tools (screenshot, click, type, wait) work on the terminal. Use tui_theme to control dark/light appearance and tui_method to choose between buffer-based reads and DOM-row reads.
29421
29916
 
29422
29917
  TIPS:
29423
29918
  - If agent_id is set and already has an active session, returns the existing one (use force_new to override)
@@ -29439,8 +29934,9 @@ TIPS:
29439
29934
  tags: exports_external2.array(exports_external2.string()).optional(),
29440
29935
  cdp_url: exports_external2.string().optional().describe("Connect to existing Chrome via CDP (e.g. http://localhost:9222). Start Chrome with --remote-debugging-port=9222"),
29441
29936
  tui_theme: exports_external2.enum(["dark", "light", "system"]).optional().default("system").describe("TUI engine only: terminal color theme. 'system' auto-detects OS dark/light mode. Choose 'light' for light backgrounds or 'dark' for dark backgrounds."),
29442
- tui_font_size: exports_external2.number().optional().default(14).describe("TUI engine only: terminal font size in pixels (default: 14). Larger = more readable screenshots, smaller = more content visible.")
29443
- }, 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, tui_theme, tui_font_size }) => {
29937
+ tui_font_size: exports_external2.number().optional().default(14).describe("TUI engine only: terminal font size in pixels (default: 14). Larger = more readable screenshots, smaller = more content visible."),
29938
+ tui_method: exports_external2.enum(["buffer", "dom"]).optional().default("buffer").describe("TUI engine only: how terminal state is read. 'buffer' reads xterm's internal buffer; 'dom' reads rendered DOM rows for a more structured browser-native view.")
29939
+ }, 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, tui_theme, tui_font_size, tui_method }) => {
29444
29940
  try {
29445
29941
  if (agent_id && !force_new) {
29446
29942
  const existing = getActiveSessionForAgent2(agent_id);
@@ -29460,7 +29956,8 @@ TIPS:
29460
29956
  storageState: storage_state,
29461
29957
  cdpUrl: cdp_url,
29462
29958
  tuiTheme: tui_theme,
29463
- tuiFontSize: tui_font_size
29959
+ tuiFontSize: tui_font_size,
29960
+ tuiMethod: tui_method
29464
29961
  });
29465
29962
  if (tags?.length) {
29466
29963
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -29918,6 +30415,13 @@ function register5(server) {
29918
30415
  });
29919
30416
  server.tool("browser_upload", "Upload a file to an input element", { session_id: exports_external2.string().optional(), selector: exports_external2.string(), file_path: exports_external2.string() }, async ({ session_id, selector, file_path }) => {
29920
30417
  try {
30418
+ if (file_path.includes("..")) {
30419
+ return err(new Error("File path must not contain '..'"));
30420
+ }
30421
+ const { existsSync: existsSync8 } = await import("fs");
30422
+ if (!existsSync8(file_path)) {
30423
+ return err(new Error(`File not found: ${file_path}`));
30424
+ }
29921
30425
  const sid = resolveSessionId(session_id);
29922
30426
  const page = getSessionPage(sid);
29923
30427
  await uploadFile(page, selector, file_path);
@@ -30631,14 +31135,14 @@ function register6(server) {
30631
31135
  } else if (/^title\s+contains\s+/i.test(trimmed)) {
30632
31136
  const needle = trimmed.replace(/^title\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
30633
31137
  result = (await getTitle(page)).toLowerCase().includes(needle.toLowerCase());
30634
- } else if (/^text:["'](.+)["']/i.test(trimmed)) {
30635
- const text = trimmed.match(/^text:["'](.+)["']/i)?.[1] ?? "";
31138
+ } else if (/^text:["']([^"']+)["']/i.test(trimmed)) {
31139
+ const text = trimmed.match(/^text:["']([^"']+)["']/i)?.[1] ?? "";
30636
31140
  result = await page.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)}) ?? false`);
30637
31141
  } else if (/^element:["'](.+)["']/i.test(trimmed)) {
30638
31142
  const sel = trimmed.match(/^element:["'](.+)["']/i)?.[1] ?? "";
30639
31143
  result = await page.evaluate(`!!document.querySelector(${JSON.stringify(sel)})`);
30640
- } else if (/^count:["'](.+)["']\s*([><=!]+)\s*(\d+)/i.test(trimmed)) {
30641
- const [, sel, op, n] = trimmed.match(/^count:["'](.+)["']\s*([><=!]+)\s*(\d+)/i);
31144
+ } else if (/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i.test(trimmed)) {
31145
+ const [, sel, op, n] = trimmed.match(/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i);
30642
31146
  const count = await page.evaluate(`document.querySelectorAll(${JSON.stringify(sel)}).length`);
30643
31147
  const num = parseInt(n);
30644
31148
  result = op === ">" ? count > num : op === ">=" ? count >= num : op === "<" ? count < num : op === "<=" ? count <= num : count === num;
@@ -34577,6 +35081,220 @@ var init_scripts2 = __esm(() => {
34577
35081
  init_helpers();
34578
35082
  });
34579
35083
 
35084
+ // src/mcp/agents.ts
35085
+ function registerAgentsAndProjects(server) {
35086
+ server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
35087
+ name: exports_external2.string(),
35088
+ description: exports_external2.string().optional(),
35089
+ session_id: exports_external2.string().optional(),
35090
+ project_id: exports_external2.string().optional(),
35091
+ working_dir: exports_external2.string().optional()
35092
+ }, async ({ name, description, session_id, project_id, working_dir }) => {
35093
+ try {
35094
+ const agent = registerAgent2(name, { description, sessionId: session_id, projectId: project_id, workingDir: working_dir });
35095
+ return json({ agent });
35096
+ } catch (e) {
35097
+ return err(e);
35098
+ }
35099
+ });
35100
+ server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external2.string() }, async ({ agent_id }) => {
35101
+ try {
35102
+ heartbeat2(agent_id);
35103
+ return json({ ok: true, agent_id, timestamp: new Date().toISOString() });
35104
+ } catch (e) {
35105
+ return err(e);
35106
+ }
35107
+ });
35108
+ server.tool("list_agents", "List all registered agents.", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
35109
+ try {
35110
+ return json({ agents: listAgents(project_id) });
35111
+ } catch (e) {
35112
+ return err(e);
35113
+ }
35114
+ });
35115
+ server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external2.string(), project_id: exports_external2.string().optional() }, async ({ agent_id, project_id }) => {
35116
+ try {
35117
+ const { updateAgent: update } = await Promise.resolve().then(() => (init_agents2(), exports_agents2));
35118
+ update(agent_id, { project_id: project_id ?? undefined });
35119
+ return json({ ok: true, agent_id, project_id });
35120
+ } catch (e) {
35121
+ return err(e);
35122
+ }
35123
+ });
35124
+ server.tool("browser_project_create", "Create or ensure a project exists", { name: exports_external2.string(), path: exports_external2.string(), description: exports_external2.string().optional() }, async ({ name, path, description }) => {
35125
+ try {
35126
+ const project = ensureProject(name, path, description);
35127
+ return json({ project });
35128
+ } catch (e) {
35129
+ return err(e);
35130
+ }
35131
+ });
35132
+ server.tool("browser_project_list", "List all registered projects", {}, async () => {
35133
+ try {
35134
+ return json({ projects: listProjects() });
35135
+ } catch (e) {
35136
+ return err(e);
35137
+ }
35138
+ });
35139
+ }
35140
+ var init_agents3 = __esm(() => {
35141
+ init_helpers();
35142
+ });
35143
+
35144
+ // src/mcp/gallery.ts
35145
+ function registerGalleryAndDownloads(server) {
35146
+ server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
35147
+ project_id: exports_external2.string().optional(),
35148
+ session_id: exports_external2.string().optional(),
35149
+ tag: exports_external2.string().optional(),
35150
+ is_favorite: exports_external2.boolean().optional(),
35151
+ date_from: exports_external2.string().optional(),
35152
+ date_to: exports_external2.string().optional(),
35153
+ limit: exports_external2.number().optional().default(50),
35154
+ offset: exports_external2.number().optional().default(0)
35155
+ }, async ({ project_id, session_id, tag, is_favorite, date_from, date_to, limit, offset }) => {
35156
+ try {
35157
+ const entries = listEntries({ projectId: project_id, sessionId: session_id, tag, isFavorite: is_favorite, dateFrom: date_from, dateTo: date_to, limit, offset });
35158
+ return json({ entries, count: entries.length });
35159
+ } catch (e) {
35160
+ return err(e);
35161
+ }
35162
+ });
35163
+ server.tool("browser_gallery_get", "Get a gallery entry by id, including thumbnail base64", { id: exports_external2.string() }, async ({ id }) => {
35164
+ try {
35165
+ const entry = getEntry(id);
35166
+ if (!entry)
35167
+ return err(new Error(`Gallery entry not found: ${id}`));
35168
+ let thumbnail_base64;
35169
+ if (entry.thumbnail_path) {
35170
+ try {
35171
+ thumbnail_base64 = Buffer.from(await Bun.file(entry.thumbnail_path).arrayBuffer()).toString("base64");
35172
+ } catch {}
35173
+ }
35174
+ return json({ entry, thumbnail_base64 });
35175
+ } catch (e) {
35176
+ return err(e);
35177
+ }
35178
+ });
35179
+ server.tool("browser_gallery_tag", "Add a tag to a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
35180
+ try {
35181
+ return json({ entry: tagEntry(id, tag) });
35182
+ } catch (e) {
35183
+ return err(e);
35184
+ }
35185
+ });
35186
+ server.tool("browser_gallery_untag", "Remove a tag from a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
35187
+ try {
35188
+ return json({ entry: untagEntry(id, tag) });
35189
+ } catch (e) {
35190
+ return err(e);
35191
+ }
35192
+ });
35193
+ server.tool("browser_gallery_favorite", "Mark or unmark a gallery entry as favorite", { id: exports_external2.string(), favorited: exports_external2.boolean() }, async ({ id, favorited }) => {
35194
+ try {
35195
+ return json({ entry: favoriteEntry(id, favorited) });
35196
+ } catch (e) {
35197
+ return err(e);
35198
+ }
35199
+ });
35200
+ server.tool("browser_gallery_delete", "Delete a gallery entry", { id: exports_external2.string() }, async ({ id }) => {
35201
+ try {
35202
+ deleteEntry(id);
35203
+ return json({ deleted: id });
35204
+ } catch (e) {
35205
+ return err(e);
35206
+ }
35207
+ });
35208
+ server.tool("browser_gallery_search", "Search gallery entries by url, title, notes, or tags", { q: exports_external2.string(), limit: exports_external2.number().optional().default(20) }, async ({ q, limit }) => {
35209
+ try {
35210
+ return json({ entries: searchEntries(q, limit) });
35211
+ } catch (e) {
35212
+ return err(e);
35213
+ }
35214
+ });
35215
+ server.tool("browser_gallery_stats", "Get gallery statistics: total, size, favorites, by-format breakdown", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
35216
+ try {
35217
+ return json(getGalleryStats(project_id));
35218
+ } catch (e) {
35219
+ return err(e);
35220
+ }
35221
+ });
35222
+ server.tool("browser_gallery_diff", "Pixel-diff two gallery screenshots. Returns diff image base64 + changed pixel count.", { id1: exports_external2.string(), id2: exports_external2.string() }, async ({ id1, id2 }) => {
35223
+ try {
35224
+ const e1 = getEntry(id1);
35225
+ const e2 = getEntry(id2);
35226
+ if (!e1)
35227
+ return err(new Error(`Gallery entry not found: ${id1}`));
35228
+ if (!e2)
35229
+ return err(new Error(`Gallery entry not found: ${id2}`));
35230
+ const result = await diffImages(e1.path, e2.path);
35231
+ return json(result);
35232
+ } catch (e) {
35233
+ return err(e);
35234
+ }
35235
+ });
35236
+ server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
35237
+ try {
35238
+ return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
35239
+ } catch (e) {
35240
+ return err(e);
35241
+ }
35242
+ });
35243
+ server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external2.string(), session_id: exports_external2.string().optional() }, async ({ id, session_id }) => {
35244
+ try {
35245
+ const file = getDownload(id, session_id);
35246
+ if (!file)
35247
+ return err(new Error(`Download not found: ${id}`));
35248
+ const base64 = Buffer.from(await Bun.file(file.path).arrayBuffer()).toString("base64");
35249
+ return json({ file, base64 });
35250
+ } catch (e) {
35251
+ return err(e);
35252
+ }
35253
+ });
35254
+ server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external2.string(), session_id: exports_external2.string().optional() }, async ({ id, session_id }) => {
35255
+ try {
35256
+ return json({ deleted: deleteDownload(id, session_id) });
35257
+ } catch (e) {
35258
+ return err(e);
35259
+ }
35260
+ });
35261
+ server.tool("browser_downloads_clean", "Delete all downloaded files older than N days (default 7)", { older_than_days: exports_external2.number().optional().default(7) }, async ({ older_than_days }) => {
35262
+ try {
35263
+ return json({ deleted_count: cleanStaleDownloads(older_than_days) });
35264
+ } catch (e) {
35265
+ return err(e);
35266
+ }
35267
+ });
35268
+ server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external2.string(), target_path: exports_external2.string(), session_id: exports_external2.string().optional() }, async ({ id, target_path, session_id }) => {
35269
+ try {
35270
+ const finalPath = exportToPath(id, target_path, session_id);
35271
+ return json({ path: finalPath });
35272
+ } catch (e) {
35273
+ return err(e);
35274
+ }
35275
+ });
35276
+ server.tool("browser_persist_file", "Persist a file permanently via open-files SDK (or local fallback)", { download_id: exports_external2.string().optional(), path: exports_external2.string().optional(), project_id: exports_external2.string().optional(), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ download_id, path: filePath, project_id, tags }) => {
35277
+ try {
35278
+ let localPath = filePath;
35279
+ if (download_id) {
35280
+ const file = getDownload(download_id);
35281
+ if (!file)
35282
+ return err(new Error(`Download not found: ${download_id}`));
35283
+ localPath = file.path;
35284
+ }
35285
+ if (!localPath)
35286
+ return err(new Error("Either download_id or path is required"));
35287
+ const result = await persistFile(localPath, { projectId: project_id, tags });
35288
+ return json(result);
35289
+ } catch (e) {
35290
+ return err(e);
35291
+ }
35292
+ });
35293
+ }
35294
+ var init_gallery2 = __esm(() => {
35295
+ init_helpers();
35296
+ });
35297
+
34580
35298
  // node_modules/@hasna/mementos/dist/index.js
34581
35299
  var exports_dist3 = {};
34582
35300
  __export(exports_dist3, {
@@ -80076,209 +80794,8 @@ Use "done" when the task is complete with {"result": "the answer"}.
80076
80794
  Keep actions simple and focused. Prefer evaluate for data extraction.`;
80077
80795
  var init_ai_task = () => {};
80078
80796
 
80079
- // src/mcp/meta.ts
80080
- function register10(server) {
80081
- server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
80082
- name: exports_external2.string(),
80083
- description: exports_external2.string().optional(),
80084
- session_id: exports_external2.string().optional().optional(),
80085
- project_id: exports_external2.string().optional(),
80086
- working_dir: exports_external2.string().optional()
80087
- }, async ({ name, description, session_id, project_id, working_dir }) => {
80088
- try {
80089
- const agent = registerAgent2(name, { description, sessionId: session_id, projectId: project_id, workingDir: working_dir });
80090
- return json({ agent });
80091
- } catch (e) {
80092
- return err(e);
80093
- }
80094
- });
80095
- server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external2.string() }, async ({ agent_id }) => {
80096
- try {
80097
- heartbeat2(agent_id);
80098
- return json({ ok: true, agent_id, timestamp: new Date().toISOString() });
80099
- } catch (e) {
80100
- return err(e);
80101
- }
80102
- });
80103
- server.tool("list_agents", "List all registered agents.", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
80104
- try {
80105
- return json({ agents: listAgents(project_id) });
80106
- } catch (e) {
80107
- return err(e);
80108
- }
80109
- });
80110
- server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external2.string(), project_id: exports_external2.string().optional() }, async ({ agent_id, project_id }) => {
80111
- try {
80112
- const { updateAgent: update } = await Promise.resolve().then(() => (init_agents2(), exports_agents2));
80113
- update(agent_id, { project_id: project_id ?? undefined });
80114
- return json({ ok: true, agent_id, project_id });
80115
- } catch (e) {
80116
- return err(e);
80117
- }
80118
- });
80119
- server.tool("browser_project_create", "Create or ensure a project exists", { name: exports_external2.string(), path: exports_external2.string(), description: exports_external2.string().optional() }, async ({ name, path, description }) => {
80120
- try {
80121
- const project = ensureProject(name, path, description);
80122
- return json({ project });
80123
- } catch (e) {
80124
- return err(e);
80125
- }
80126
- });
80127
- server.tool("browser_project_list", "List all registered projects", {}, async () => {
80128
- try {
80129
- return json({ projects: listProjects() });
80130
- } catch (e) {
80131
- return err(e);
80132
- }
80133
- });
80134
- server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
80135
- project_id: exports_external2.string().optional(),
80136
- session_id: exports_external2.string().optional().optional(),
80137
- tag: exports_external2.string().optional(),
80138
- is_favorite: exports_external2.boolean().optional(),
80139
- date_from: exports_external2.string().optional(),
80140
- date_to: exports_external2.string().optional(),
80141
- limit: exports_external2.number().optional().default(50),
80142
- offset: exports_external2.number().optional().default(0)
80143
- }, async ({ project_id, session_id, tag, is_favorite, date_from, date_to, limit, offset }) => {
80144
- try {
80145
- const entries = listEntries({ projectId: project_id, sessionId: session_id, tag, isFavorite: is_favorite, dateFrom: date_from, dateTo: date_to, limit, offset });
80146
- return json({ entries, count: entries.length });
80147
- } catch (e) {
80148
- return err(e);
80149
- }
80150
- });
80151
- server.tool("browser_gallery_get", "Get a gallery entry by id, including thumbnail base64", { id: exports_external2.string() }, async ({ id }) => {
80152
- try {
80153
- const entry = getEntry(id);
80154
- if (!entry)
80155
- return err(new Error(`Gallery entry not found: ${id}`));
80156
- let thumbnail_base64;
80157
- if (entry.thumbnail_path) {
80158
- try {
80159
- thumbnail_base64 = Buffer.from(await Bun.file(entry.thumbnail_path).arrayBuffer()).toString("base64");
80160
- } catch {}
80161
- }
80162
- return json({ entry, thumbnail_base64 });
80163
- } catch (e) {
80164
- return err(e);
80165
- }
80166
- });
80167
- server.tool("browser_gallery_tag", "Add a tag to a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
80168
- try {
80169
- return json({ entry: tagEntry(id, tag) });
80170
- } catch (e) {
80171
- return err(e);
80172
- }
80173
- });
80174
- server.tool("browser_gallery_untag", "Remove a tag from a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
80175
- try {
80176
- return json({ entry: untagEntry(id, tag) });
80177
- } catch (e) {
80178
- return err(e);
80179
- }
80180
- });
80181
- server.tool("browser_gallery_favorite", "Mark or unmark a gallery entry as favorite", { id: exports_external2.string(), favorited: exports_external2.boolean() }, async ({ id, favorited }) => {
80182
- try {
80183
- return json({ entry: favoriteEntry(id, favorited) });
80184
- } catch (e) {
80185
- return err(e);
80186
- }
80187
- });
80188
- server.tool("browser_gallery_delete", "Delete a gallery entry", { id: exports_external2.string() }, async ({ id }) => {
80189
- try {
80190
- deleteEntry(id);
80191
- return json({ deleted: id });
80192
- } catch (e) {
80193
- return err(e);
80194
- }
80195
- });
80196
- server.tool("browser_gallery_search", "Search gallery entries by url, title, notes, or tags", { q: exports_external2.string(), limit: exports_external2.number().optional().default(20) }, async ({ q, limit }) => {
80197
- try {
80198
- return json({ entries: searchEntries(q, limit) });
80199
- } catch (e) {
80200
- return err(e);
80201
- }
80202
- });
80203
- server.tool("browser_gallery_stats", "Get gallery statistics: total, size, favorites, by-format breakdown", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
80204
- try {
80205
- return json(getGalleryStats(project_id));
80206
- } catch (e) {
80207
- return err(e);
80208
- }
80209
- });
80210
- server.tool("browser_gallery_diff", "Pixel-diff two gallery screenshots. Returns diff image base64 + changed pixel count.", { id1: exports_external2.string(), id2: exports_external2.string() }, async ({ id1, id2 }) => {
80211
- try {
80212
- const e1 = getEntry(id1);
80213
- const e2 = getEntry(id2);
80214
- if (!e1)
80215
- return err(new Error(`Gallery entry not found: ${id1}`));
80216
- if (!e2)
80217
- return err(new Error(`Gallery entry not found: ${id2}`));
80218
- const result = await diffImages(e1.path, e2.path);
80219
- return json(result);
80220
- } catch (e) {
80221
- return err(e);
80222
- }
80223
- });
80224
- server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional().optional() }, async ({ session_id }) => {
80225
- try {
80226
- return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
80227
- } catch (e) {
80228
- return err(e);
80229
- }
80230
- });
80231
- server.tool("browser_downloads_get", "Get a downloaded file by id, returning base64 content and metadata", { id: exports_external2.string(), session_id: exports_external2.string().optional().optional() }, async ({ id, session_id }) => {
80232
- try {
80233
- const file = getDownload(id, session_id);
80234
- if (!file)
80235
- return err(new Error(`Download not found: ${id}`));
80236
- const base64 = Buffer.from(await Bun.file(file.path).arrayBuffer()).toString("base64");
80237
- return json({ file, base64 });
80238
- } catch (e) {
80239
- return err(e);
80240
- }
80241
- });
80242
- server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external2.string(), session_id: exports_external2.string().optional().optional() }, async ({ id, session_id }) => {
80243
- try {
80244
- const deleted = deleteDownload(id, session_id);
80245
- return json({ deleted });
80246
- } catch (e) {
80247
- return err(e);
80248
- }
80249
- });
80250
- server.tool("browser_downloads_clean", "Delete all downloaded files older than N days (default 7)", { older_than_days: exports_external2.number().optional().default(7) }, async ({ older_than_days }) => {
80251
- try {
80252
- return json({ deleted_count: cleanStaleDownloads(older_than_days) });
80253
- } catch (e) {
80254
- return err(e);
80255
- }
80256
- });
80257
- server.tool("browser_downloads_export", "Copy a downloaded file to a target path", { id: exports_external2.string(), target_path: exports_external2.string(), session_id: exports_external2.string().optional().optional() }, async ({ id, target_path, session_id }) => {
80258
- try {
80259
- const finalPath = exportToPath(id, target_path, session_id);
80260
- return json({ path: finalPath });
80261
- } catch (e) {
80262
- return err(e);
80263
- }
80264
- });
80265
- server.tool("browser_persist_file", "Persist a file permanently via open-files SDK (or local fallback)", { download_id: exports_external2.string().optional(), path: exports_external2.string().optional(), project_id: exports_external2.string().optional(), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ download_id, path: filePath, project_id, tags }) => {
80266
- try {
80267
- let localPath = filePath;
80268
- if (download_id) {
80269
- const file = getDownload(download_id);
80270
- if (!file)
80271
- return err(new Error(`Download not found: ${download_id}`));
80272
- localPath = file.path;
80273
- }
80274
- if (!localPath)
80275
- return err(new Error("Either download_id or path is required"));
80276
- const result = await persistFile(localPath, { projectId: project_id, tags });
80277
- return json(result);
80278
- } catch (e) {
80279
- return err(e);
80280
- }
80281
- });
80797
+ // src/mcp/integration.ts
80798
+ function registerIntegrationAndMeta(server) {
80282
80799
  const activeWatchHandles2 = new Map;
80283
80800
  server.tool("browser_watch_start", "Start watching a page for DOM changes", { session_id: exports_external2.string().optional(), selector: exports_external2.string().optional(), interval_ms: exports_external2.number().optional().default(500), max_changes: exports_external2.number().optional().default(50) }, async ({ session_id, selector, interval_ms, max_changes }) => {
80284
80801
  try {
@@ -80308,214 +80825,7 @@ function register10(server) {
80308
80825
  return err(e);
80309
80826
  }
80310
80827
  });
80311
- server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
80312
- try {
80313
- const groups = {
80314
- Navigation: [
80315
- { tool: "browser_navigate", description: "Navigate to a URL" },
80316
- { tool: "browser_back", description: "Navigate back in history" },
80317
- { tool: "browser_forward", description: "Navigate forward in history" },
80318
- { tool: "browser_reload", description: "Reload the current page" },
80319
- { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
80320
- { tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
80321
- ],
80322
- Interaction: [
80323
- { tool: "browser_click", description: "Click element by ref or selector" },
80324
- { tool: "browser_click_text", description: "Click element by visible text" },
80325
- { tool: "browser_type", description: "Type text into an element" },
80326
- { tool: "browser_hover", description: "Hover over an element" },
80327
- { tool: "browser_scroll", description: "Scroll the page" },
80328
- { tool: "browser_select", description: "Select a dropdown option" },
80329
- { tool: "browser_toggle", description: "Check/uncheck a checkbox" },
80330
- { tool: "browser_upload", description: "Upload a file to an input" },
80331
- { tool: "browser_press_key", description: "Press a keyboard key" },
80332
- { tool: "browser_wait", description: "Wait for a selector to appear" },
80333
- { tool: "browser_wait_for_text", description: "Wait for text to appear" },
80334
- { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
80335
- { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
80336
- { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
80337
- ],
80338
- Extraction: [
80339
- { tool: "browser_get_text", description: "Get text content from page/selector" },
80340
- { tool: "browser_get_html", description: "Get HTML content from page/selector" },
80341
- { tool: "browser_get_links", description: "Get all links on the page" },
80342
- { tool: "browser_get_page_info", description: "Full page summary in one call" },
80343
- { tool: "browser_extract", description: "Extract content in various formats" },
80344
- { tool: "browser_find", description: "Find elements by selector" },
80345
- { tool: "browser_element_exists", description: "Check if a selector exists" },
80346
- { tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
80347
- { tool: "browser_evaluate", description: "Execute JavaScript in page context" }
80348
- ],
80349
- Capture: [
80350
- { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
80351
- { tool: "browser_pdf", description: "Generate a PDF of the page" },
80352
- { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
80353
- { tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" },
80354
- { tool: "browser_diff", description: "Visual diff between two URLs \u2014 highlights changes in red" }
80355
- ],
80356
- Storage: [
80357
- { tool: "browser_cookies_get", description: "Get cookies" },
80358
- { tool: "browser_cookies_set", description: "Set a cookie" },
80359
- { tool: "browser_cookies_clear", description: "Clear cookies" },
80360
- { tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
80361
- { tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
80362
- { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
80363
- { tool: "browser_profile_load", description: "Load and apply a saved profile" },
80364
- { tool: "browser_profile_list", description: "List saved profiles" },
80365
- { tool: "browser_profile_delete", description: "Delete a saved profile" },
80366
- { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
80367
- { tool: "browser_session_list_states", description: "List saved storage states" },
80368
- { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
80369
- ],
80370
- Network: [
80371
- { tool: "browser_network_log", description: "Get captured network requests" },
80372
- { tool: "browser_network_intercept", description: "Add a network interception rule" },
80373
- { tool: "browser_har_start", description: "Start HAR capture" },
80374
- { tool: "browser_har_stop", description: "Stop HAR capture and get data" },
80375
- { tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
80376
- { tool: "browser_intercept_clear", description: "Remove all response intercepts" }
80377
- ],
80378
- Performance: [
80379
- { tool: "browser_performance", description: "Get performance metrics" },
80380
- { tool: "browser_performance_budget", description: "Check perf against budget thresholds (LCP, FCP, CLS, TTFB)" }
80381
- ],
80382
- Console: [
80383
- { tool: "browser_console_log", description: "Get console messages" },
80384
- { tool: "browser_has_errors", description: "Check for console errors" },
80385
- { tool: "browser_clear_errors", description: "Clear console error log" },
80386
- { tool: "browser_get_dialogs", description: "Get pending dialogs" }
80387
- ],
80388
- Recording: [
80389
- { tool: "browser_record_start", description: "Start recording actions" },
80390
- { tool: "browser_record_step", description: "Add a step to recording" },
80391
- { tool: "browser_record_stop", description: "Stop and save recording" },
80392
- { tool: "browser_record_replay", description: "Replay a recorded sequence" },
80393
- { tool: "browser_record_export", description: "Export recording as Playwright test, Puppeteer script, or JSON" },
80394
- { tool: "browser_recordings_list", description: "List all recordings" }
80395
- ],
80396
- Auth: [
80397
- { tool: "browser_auth_record", description: "Start recording a login flow" },
80398
- { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
80399
- { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
80400
- { tool: "browser_auth_list", description: "List all saved auth flows" },
80401
- { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
80402
- ],
80403
- Workflows: [
80404
- { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
80405
- { tool: "browser_workflow_list", description: "List all saved workflows" },
80406
- { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
80407
- { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
80408
- ],
80409
- Data: [
80410
- { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
80411
- { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
80412
- { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
80413
- { tool: "browser_dataset_list", description: "List all saved datasets" },
80414
- { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
80415
- { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
80416
- ],
80417
- Crawl: [
80418
- { tool: "browser_crawl", description: "Crawl a URL recursively" }
80419
- ],
80420
- Agent: [
80421
- { tool: "register_agent", description: "Register an agent session" },
80422
- { tool: "heartbeat", description: "Update agent last_seen_at" },
80423
- { tool: "list_agents", description: "List registered agents" },
80424
- { tool: "set_focus", description: "Set active project context" }
80425
- ],
80426
- Project: [
80427
- { tool: "browser_project_create", description: "Create or ensure a project" },
80428
- { tool: "browser_project_list", description: "List all projects" }
80429
- ],
80430
- Gallery: [
80431
- { tool: "browser_gallery_list", description: "List screenshot gallery entries" },
80432
- { tool: "browser_gallery_get", description: "Get a gallery entry by id" },
80433
- { tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
80434
- { tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
80435
- { tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
80436
- { tool: "browser_gallery_delete", description: "Delete a gallery entry" },
80437
- { tool: "browser_gallery_search", description: "Search gallery entries" },
80438
- { tool: "browser_gallery_stats", description: "Get gallery statistics" },
80439
- { tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
80440
- ],
80441
- Downloads: [
80442
- { tool: "browser_downloads_list", description: "List downloaded files" },
80443
- { tool: "browser_downloads_get", description: "Get a download by id" },
80444
- { tool: "browser_downloads_delete", description: "Delete a download" },
80445
- { tool: "browser_downloads_clean", description: "Clean old downloads" },
80446
- { tool: "browser_downloads_export", description: "Copy download to a path" },
80447
- { tool: "browser_persist_file", description: "Persist file permanently" }
80448
- ],
80449
- Session: [
80450
- { tool: "browser_session_create", description: "Create a new browser session" },
80451
- { tool: "browser_session_list", description: "List all sessions" },
80452
- { tool: "browser_session_close", description: "Close a session" },
80453
- { tool: "browser_session_get_by_name", description: "Get session by name" },
80454
- { tool: "browser_session_rename", description: "Rename a session" },
80455
- { tool: "browser_session_lock", description: "Lock a session for an agent" },
80456
- { tool: "browser_session_unlock", description: "Unlock a session" },
80457
- { tool: "browser_session_transfer", description: "Transfer session to another agent" },
80458
- { tool: "browser_session_tag", description: "Add a tag to a session" },
80459
- { tool: "browser_session_untag", description: "Remove a tag from a session" },
80460
- { tool: "browser_session_stats", description: "Get session stats and token usage" },
80461
- { tool: "browser_session_timeline", description: "Get chronological action log" },
80462
- { tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
80463
- { tool: "browser_tab_new", description: "Open a new tab" },
80464
- { tool: "browser_tab_list", description: "List all open tabs" },
80465
- { tool: "browser_tab_switch", description: "Switch to a tab by index" },
80466
- { tool: "browser_tab_close", description: "Close a tab by index" }
80467
- ],
80468
- TUI: [
80469
- { tool: "browser_tui_send_keys", description: "Send keystrokes (ctrl+c, arrow_up, tab, enter, etc.)" },
80470
- { tool: "browser_tui_send_text", description: "Type text + optional Enter (most common TUI interaction)" },
80471
- { tool: "browser_tui_resize", description: "Resize terminal cols/rows mid-session" },
80472
- { tool: "browser_tui_get_text", description: "Get terminal text buffer (full or row range)" },
80473
- { tool: "browser_tui_wait_for_text", description: "Wait for text to appear in terminal output" },
80474
- { tool: "browser_tui_get_cursor", description: "Get cursor position (row, col)" },
80475
- { tool: "browser_tui_assert", description: "Assert terminal conditions (text contains, row N contains, cursor at)" },
80476
- { tool: "browser_tui_snapshot", description: "Structured terminal snapshot (rows array, cursor, dimensions)" },
80477
- { tool: "browser_tui_record_start", description: "Start recording terminal as asciicast" },
80478
- { tool: "browser_tui_record_stop", description: "Stop recording, return asciicast v2 JSON" }
80479
- ],
80480
- Meta: [
80481
- { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
80482
- { tool: "browser_version", description: "Show running binary version and tool count" },
80483
- { tool: "browser_help", description: "Show this help (all tools)" },
80484
- { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
80485
- { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
80486
- { tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
80487
- { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
80488
- { tool: "browser_watch_start", description: "Watch page for DOM changes" },
80489
- { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
80490
- { tool: "browser_watch_stop", description: "Stop DOM watcher" },
80491
- { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
80492
- ]
80493
- };
80494
- const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
80495
- return json({ groups, total_tools: totalTools });
80496
- } catch (e) {
80497
- return err(e);
80498
- }
80499
- });
80500
- server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
80501
- try {
80502
- const { getDataDir: getDataDir7 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
80503
- const toolCount = Object.keys(server._registeredTools ?? {}).length;
80504
- const { readFileSync: readFileSync12 } = await import("fs");
80505
- const { join: join27 } = await import("path");
80506
- const _pkg = JSON.parse(readFileSync12(join27(import.meta.dir, "../../package.json"), "utf8"));
80507
- return json({
80508
- version: _pkg.version,
80509
- mcp_tools_count: toolCount,
80510
- bun_version: Bun.version,
80511
- data_dir: getDataDir7(),
80512
- node_env: process.env["NODE_ENV"] ?? "production"
80513
- });
80514
- } catch (e) {
80515
- return err(e);
80516
- }
80517
- });
80518
- 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_external2.string().optional(), service: exports_external2.string(), login_url: exports_external2.string().optional(), save_profile: exports_external2.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
80828
+ server.tool("browser_secrets_login", "Login to a service using credentials from open-secrets vault or ~/.secrets.", { session_id: exports_external2.string().optional(), service: exports_external2.string(), login_url: exports_external2.string().optional(), save_profile: exports_external2.boolean().optional().default(true) }, async ({ session_id, service, login_url, save_profile }) => {
80519
80829
  try {
80520
80830
  const sid = resolveSessionId(session_id);
80521
80831
  const page = getSessionPage(sid);
@@ -80532,7 +80842,7 @@ function register10(server) {
80532
80842
  return err(e);
80533
80843
  }
80534
80844
  });
80535
- server.tool("browser_remember", "Store page facts in open-mementos for future recall. Agents skip re-scraping on repeat visits.", { session_id: exports_external2.string().optional(), facts: exports_external2.record(exports_external2.unknown()), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ session_id, facts, tags }) => {
80845
+ server.tool("browser_remember", "Store page facts in open-mementos for future recall.", { session_id: exports_external2.string().optional(), facts: exports_external2.record(exports_external2.unknown()), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ session_id, facts, tags }) => {
80536
80846
  try {
80537
80847
  const sid = resolveSessionId(session_id);
80538
80848
  const page = getSessionPage(sid);
@@ -80544,7 +80854,7 @@ function register10(server) {
80544
80854
  return err(e);
80545
80855
  }
80546
80856
  });
80547
- server.tool("browser_recall", "Retrieve cached page facts from open-mementos. Returns null if not cached or expired.", { url: exports_external2.string(), max_age_hours: exports_external2.number().optional().default(24) }, async ({ url, max_age_hours }) => {
80857
+ server.tool("browser_recall", "Retrieve cached page facts from open-mementos.", { url: exports_external2.string(), max_age_hours: exports_external2.number().optional().default(24) }, async ({ url, max_age_hours }) => {
80548
80858
  try {
80549
80859
  const { recallPage: recallPage2 } = await Promise.resolve().then(() => (init_page_memory(), exports_page_memory));
80550
80860
  const memory = await recallPage2(url, max_age_hours);
@@ -80565,7 +80875,7 @@ function register10(server) {
80565
80875
  return err(e);
80566
80876
  }
80567
80877
  });
80568
- server.tool("browser_check_navigation", "Check if another agent is already scraping this URL. Prevents duplicate work across agents.", { url: exports_external2.string() }, async ({ url }) => {
80878
+ server.tool("browser_check_navigation", "Check if another agent is already scraping this URL.", { url: exports_external2.string() }, async ({ url }) => {
80569
80879
  try {
80570
80880
  const { checkDuplicate: checkDuplicate3 } = await Promise.resolve().then(() => (init_coordination(), exports_coordination));
80571
80881
  return json(await checkDuplicate3(url));
@@ -80599,7 +80909,7 @@ function register10(server) {
80599
80909
  return err(e);
80600
80910
  }
80601
80911
  });
80602
- 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_external2.string().optional(), skill: exports_external2.string(), params: exports_external2.record(exports_external2.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
80912
+ server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing, monitor-price, etc.).", { session_id: exports_external2.string().optional(), skill: exports_external2.string(), params: exports_external2.record(exports_external2.unknown()).optional().default({}) }, async ({ session_id, skill, params }) => {
80603
80913
  try {
80604
80914
  const sid = resolveSessionId(session_id);
80605
80915
  const page = getSessionPage(sid);
@@ -80617,7 +80927,7 @@ function register10(server) {
80617
80927
  return err(e);
80618
80928
  }
80619
80929
  });
80620
- server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot. Eliminates 80% of round trips for multi-step flows.", {
80930
+ server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot.", {
80621
80931
  session_id: exports_external2.string().optional(),
80622
80932
  actions: exports_external2.array(exports_external2.object({
80623
80933
  tool: exports_external2.string(),
@@ -80656,8 +80966,8 @@ function register10(server) {
80656
80966
  break;
80657
80967
  case "fill_form":
80658
80968
  if (args.fields) {
80659
- const { fillForm: fillForm3 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
80660
- const r = await fillForm3(page, args.fields);
80969
+ const { fillForm: fillForm2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
80970
+ const r = await fillForm2(page, args.fields);
80661
80971
  results.push({ tool: action.tool, success: true, result: r });
80662
80972
  }
80663
80973
  break;
@@ -80707,13 +81017,13 @@ function register10(server) {
80707
81017
  return err(e);
80708
81018
  }
80709
81019
  });
80710
- server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
81020
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel.", {
80711
81021
  actions: exports_external2.array(exports_external2.object({
80712
- session_id: exports_external2.string().describe("Target session ID"),
80713
- tool: exports_external2.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
81022
+ session_id: exports_external2.string(),
81023
+ tool: exports_external2.string(),
80714
81024
  args: exports_external2.record(exports_external2.unknown()).optional().default({})
80715
81025
  })),
80716
- timeout: exports_external2.number().optional().default(30000).describe("Timeout per action in ms")
81026
+ timeout: exports_external2.number().optional().default(30000)
80717
81027
  }, async ({ actions, timeout }) => {
80718
81028
  try {
80719
81029
  const t0 = Date.now();
@@ -80775,22 +81085,21 @@ function register10(server) {
80775
81085
  }
80776
81086
  });
80777
81087
  const results = await Promise.all(promises);
80778
- const duration_ms = Date.now() - t0;
80779
81088
  const succeeded = results.filter((r) => r.success).length;
80780
81089
  const failed = results.filter((r) => !r.success).length;
80781
- return json({ results, duration_ms, succeeded, failed, total: actions.length });
81090
+ return json({ results, duration_ms: Date.now() - t0, succeeded, failed, total: actions.length });
80782
81091
  } catch (e) {
80783
81092
  return err(e);
80784
81093
  }
80785
81094
  });
80786
81095
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
80787
81096
  try {
80788
- return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
81097
+ return json({ message: "Session pool not yet implemented in this version.", ready: 0, total: 0 });
80789
81098
  } catch (e) {
80790
81099
  return err(e);
80791
81100
  }
80792
81101
  });
80793
- server.tool("browser_cron_create", "Schedule a browser task to run automatically. Uses Bun.cron. Example: '0 9 * * 1' = Monday 9am.", { schedule: exports_external2.string(), url: exports_external2.string().optional(), skill: exports_external2.string().optional(), extract: exports_external2.record(exports_external2.string()).optional(), name: exports_external2.string().optional() }, async ({ schedule, url, skill, extract: extract2, name }) => {
81102
+ server.tool("browser_cron_create", "Schedule a browser task to run automatically.", { schedule: exports_external2.string(), url: exports_external2.string().optional(), skill: exports_external2.string().optional(), extract: exports_external2.record(exports_external2.string()).optional(), name: exports_external2.string().optional() }, async ({ schedule, url, skill, extract: extract2, name }) => {
80794
81103
  try {
80795
81104
  const { createCronJob: createCronJob2 } = await Promise.resolve().then(() => (init_cron_manager(), exports_cron_manager));
80796
81105
  return json(createCronJob2(schedule, { url, skill, extract: extract2 }, name));
@@ -80830,7 +81139,7 @@ function register10(server) {
80830
81139
  return err(e);
80831
81140
  }
80832
81141
  });
80833
- server.tool("browser_watch_url", "Monitor a URL for content changes on a schedule. Stores change events.", { url: exports_external2.string(), schedule: exports_external2.string().optional().default("*/5 * * * *"), selector: exports_external2.string().optional(), name: exports_external2.string().optional() }, async ({ url, schedule, selector, name }) => {
81142
+ server.tool("browser_watch_url", "Monitor a URL for content changes on a schedule.", { url: exports_external2.string(), schedule: exports_external2.string().optional().default("*/5 * * * *"), selector: exports_external2.string().optional(), name: exports_external2.string().optional() }, async ({ url, schedule, selector, name }) => {
80834
81143
  try {
80835
81144
  const { createWatchJob: createWatchJob2 } = await Promise.resolve().then(() => (init_url_watcher(), exports_url_watcher));
80836
81145
  return json(createWatchJob2(url, schedule, { name, selector }));
@@ -80862,7 +81171,7 @@ function register10(server) {
80862
81171
  return err(e);
80863
81172
  }
80864
81173
  });
80865
- server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku. Returns result + steps taken.", { session_id: exports_external2.string().optional(), task: exports_external2.string(), max_steps: exports_external2.number().optional().default(10), model: exports_external2.string().optional() }, async ({ session_id, task, max_steps, model }) => {
81174
+ server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku.", { session_id: exports_external2.string().optional(), task: exports_external2.string(), max_steps: exports_external2.number().optional().default(10), model: exports_external2.string().optional() }, async ({ session_id, task, max_steps, model }) => {
80866
81175
  try {
80867
81176
  const sid = resolveSessionId(session_id);
80868
81177
  const page = getSessionPage(sid);
@@ -80873,10 +81182,22 @@ function register10(server) {
80873
81182
  }
80874
81183
  });
80875
81184
  }
80876
- var init_meta = __esm(() => {
81185
+ var init_integration = __esm(() => {
80877
81186
  init_helpers();
80878
81187
  });
80879
81188
 
81189
+ // src/mcp/meta.ts
81190
+ function register10(server) {
81191
+ registerAgentsAndProjects(server);
81192
+ registerGalleryAndDownloads(server);
81193
+ registerIntegrationAndMeta(server);
81194
+ }
81195
+ var init_meta = __esm(() => {
81196
+ init_agents3();
81197
+ init_gallery2();
81198
+ init_integration();
81199
+ });
81200
+
80880
81201
  // src/mcp/data.ts
80881
81202
  function register11(server) {
80882
81203
  register8(server);
@@ -80891,32 +81212,74 @@ var init_data = __esm(() => {
80891
81212
 
80892
81213
  // src/mcp/tui.ts
80893
81214
  function assertTuiSession(sessionId) {
80894
- const { getSessionEngine: getSessionEngine2 } = (init_session(), __toCommonJS(exports_session));
80895
- const engine = getSessionEngine2(sessionId);
81215
+ const engine = getSessionEngine(sessionId);
80896
81216
  if (engine !== "tui") {
80897
- throw new Error(`browser_tui_* tools require a TUI session (engine="tui"), but this session uses engine="${engine}". Create a TUI session with: browser_session_create(engine="tui", start_url="your-command")`);
81217
+ throw new Error(`browser_tui_* tools require a TUI session (engine="tui"), but session uses engine="${engine}". Create one with: browser_session_create(engine="tui", start_url="your-command")`);
80898
81218
  }
80899
81219
  }
80900
- async function getTermText(page, startRow, endRow) {
80901
- const result = await page.evaluate((args) => {
80902
- const [sr, er] = args;
80903
- const term = window.term ?? window.terminal;
80904
- if (!term?.buffer?.active)
80905
- return { text: "", rows: [], row_count: 0 };
80906
- const buf = term.buffer.active;
80907
- const allRows = [];
80908
- for (let i = 0;i < buf.length; i++) {
80909
- const line = buf.getLine(i);
80910
- if (line)
80911
- allRows.push(line.translateToString(true));
80912
- }
80913
- const start = sr ?? 0;
80914
- const end = er ?? allRows.length;
80915
- const filtered = allRows.slice(start, end);
80916
- return { text: filtered.join(`
80917
- `).trimEnd(), rows: filtered, row_count: allRows.length };
80918
- }, [startRow, endRow]);
80919
- return result;
81220
+ function getTuiSession(sessionId) {
81221
+ return getSessionTuiSession(sessionId);
81222
+ }
81223
+ function getTuiMeta(sessionId) {
81224
+ const session = getTuiSession(sessionId);
81225
+ return {
81226
+ method: session.method,
81227
+ reconnected: session.reconnectCount > 0
81228
+ };
81229
+ }
81230
+ function withMeta(sessionId, data) {
81231
+ return { ...data, ...getTuiMeta(sessionId) };
81232
+ }
81233
+ function withStableMeta(sessionId, data) {
81234
+ return { ...data, stuck: false, ...getTuiMeta(sessionId) };
81235
+ }
81236
+ function filterRows(rows, startRow, endRow) {
81237
+ const start = startRow ?? 0;
81238
+ const end = endRow ?? rows.length;
81239
+ const filtered = rows.slice(start, end);
81240
+ return {
81241
+ text: filtered.join(`
81242
+ `).trimEnd(),
81243
+ rows: filtered
81244
+ };
81245
+ }
81246
+ async function withTuiHealth(sessionId, operation, options = {}) {
81247
+ const {
81248
+ timeoutMs = DEFAULT_TOOL_TIMEOUT_MS2,
81249
+ reconnectOnStuck = RECONNECT_ON_STUCK,
81250
+ operationName = "operation"
81251
+ } = options;
81252
+ let session = getTuiSession(sessionId);
81253
+ let page = getSessionPage(sessionId);
81254
+ const health = await isTuiHealthy(session);
81255
+ if (!health.healthy && reconnectOnStuck && session.reconnectCount < 2) {
81256
+ try {
81257
+ const { getSessionCommand: getSessionCommand2, setSessionTui: setSessionTui2 } = await Promise.resolve().then(() => (init_session(), exports_session));
81258
+ const cmd = getSessionCommand2?.(sessionId) ?? "bash";
81259
+ const newSession = await reconnectTui(session, cmd, { method: session.method });
81260
+ setSessionTui2(sessionId, newSession);
81261
+ session = newSession;
81262
+ page = newSession.page;
81263
+ } catch {}
81264
+ } else if (!health.healthy) {
81265
+ throw Object.assign(new Error(`TUI session is unhealthy: ${health.reason}. Close and reopen the session.`), { code: "TUI_UNHEALTHY" });
81266
+ }
81267
+ let timedOut = false;
81268
+ const timer = setTimeout(() => {
81269
+ timedOut = true;
81270
+ }, timeoutMs);
81271
+ try {
81272
+ return await operation(page, session);
81273
+ } catch (error) {
81274
+ if (timedOut) {
81275
+ const err2 = new Error(`${operationName} timed out after ${timeoutMs}ms \u2014 ttyd/playwright connection may be unhealthy. Status: ${health.healthy ? "was healthy before op" : "was already unhealthy"}. Try closing and re-opening the session.`);
81276
+ Object.assign(err2, { code: "TUI_TIMEOUT" });
81277
+ throw err2;
81278
+ }
81279
+ throw error;
81280
+ } finally {
81281
+ clearTimeout(timer);
81282
+ }
80920
81283
  }
80921
81284
  function register12(server) {
80922
81285
  server.tool("browser_tui_send_keys", `Send keystrokes to a TUI terminal session. Use friendly key names.
@@ -80931,103 +81294,123 @@ SUPPORTED KEYS:
80931
81294
  Pass multiple keys as a comma-separated string: "tab,tab,enter" or "ctrl+c"
80932
81295
  For typing text, use browser_tui_send_text instead.`, {
80933
81296
  session_id: exports_external2.string().optional(),
80934
- keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'")
80935
- }, async ({ session_id, keys }) => {
81297
+ keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'"),
81298
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81299
+ }, async ({ session_id, keys, timeout_ms }) => {
80936
81300
  try {
80937
81301
  const sid = resolveSessionId(session_id);
80938
81302
  assertTuiSession(sid);
80939
- const page = getSessionPage(sid);
80940
- const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
80941
- const sent = [];
80942
- for (const key of keyList) {
80943
- const mapped = KEY_MAP[key];
80944
- if (mapped) {
80945
- if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
80946
- await page.keyboard.insertText(mapped);
81303
+ const result = await withTuiHealth(sid, async (page) => {
81304
+ const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
81305
+ const sent = [];
81306
+ for (const key of keyList) {
81307
+ const mapped = KEY_MAP[key];
81308
+ if (mapped) {
81309
+ if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
81310
+ await page.keyboard.insertText(mapped);
81311
+ } else {
81312
+ await page.keyboard.press(mapped);
81313
+ }
81314
+ sent.push(key);
80947
81315
  } else {
80948
- await page.keyboard.press(mapped);
81316
+ await page.keyboard.press(key);
81317
+ sent.push(key);
80949
81318
  }
80950
- sent.push(key);
80951
- } else {
80952
- await page.keyboard.press(key);
80953
- sent.push(key);
80954
81319
  }
80955
- }
80956
- return json({ sent, count: sent.length });
81320
+ return { sent, count: sent.length };
81321
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_send_keys" });
81322
+ return json(withStableMeta(sid, result));
80957
81323
  } catch (e) {
81324
+ if (e.code === "TUI_TIMEOUT")
81325
+ return err(e);
81326
+ if (e.code === "TUI_UNHEALTHY")
81327
+ return err(e);
80958
81328
  return err(e);
80959
81329
  }
80960
81330
  });
80961
- server.tool("browser_tui_send_text", `Type text into a TUI terminal and optionally press Enter. This is the most common way to interact with terminal apps.
80962
-
80963
- Examples:
80964
- - Send a command: text="ls -la", press_enter=true
80965
- - Type without executing: text="partial input", press_enter=false
80966
- - Send to a prompt: text="yes", press_enter=true`, {
81331
+ server.tool("browser_tui_send_text", `Type text into a TUI terminal and optionally press Enter. This is the most common way to interact with terminal apps.`, {
80967
81332
  session_id: exports_external2.string().optional(),
80968
81333
  text: exports_external2.string().describe("Text to type into the terminal"),
80969
- press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)")
80970
- }, async ({ session_id, text, press_enter }) => {
81334
+ press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)"),
81335
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81336
+ }, async ({ session_id, text, press_enter, timeout_ms }) => {
80971
81337
  try {
80972
81338
  const sid = resolveSessionId(session_id);
80973
81339
  assertTuiSession(sid);
80974
- const page = getSessionPage(sid);
80975
- const textarea = await page.$(".xterm-helper-textarea");
80976
- if (textarea) {
80977
- await textarea.type(text);
80978
- } else {
80979
- await page.keyboard.type(text);
80980
- }
80981
- if (press_enter) {
80982
- await page.keyboard.press("Enter");
80983
- }
80984
- return json({ typed: text, pressed_enter: press_enter });
81340
+ const result = await withTuiHealth(sid, async (page) => {
81341
+ const textarea = await page.$(".xterm-helper-textarea");
81342
+ if (textarea) {
81343
+ await textarea.type(text);
81344
+ } else {
81345
+ await page.keyboard.type(text);
81346
+ }
81347
+ if (press_enter)
81348
+ await page.keyboard.press("Enter");
81349
+ return { typed: text, pressed_enter: press_enter };
81350
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_send_text" });
81351
+ return json(withStableMeta(sid, result));
80985
81352
  } catch (e) {
81353
+ if (e.code === "TUI_TIMEOUT")
81354
+ return err(e);
81355
+ if (e.code === "TUI_UNHEALTHY")
81356
+ return err(e);
80986
81357
  return err(e);
80987
81358
  }
80988
81359
  });
80989
- server.tool("browser_tui_resize", "Resize the terminal to a specific number of columns and rows. Useful for testing responsive TUI layouts at different terminal sizes.", {
81360
+ server.tool("browser_tui_resize", "Resize the terminal to a specific number of columns and rows.", {
80990
81361
  session_id: exports_external2.string().optional(),
80991
81362
  cols: exports_external2.number().describe("Number of columns (e.g. 80, 120, 200)"),
80992
- rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)")
80993
- }, async ({ session_id, cols, rows }) => {
81363
+ rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)"),
81364
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81365
+ }, async ({ session_id, cols, rows, timeout_ms }) => {
80994
81366
  try {
80995
81367
  const sid = resolveSessionId(session_id);
80996
81368
  assertTuiSession(sid);
80997
- const page = getSessionPage(sid);
80998
- const result = await page.evaluate((args) => {
80999
- const [c, r] = args;
81000
- const term = window.term ?? window.terminal;
81001
- if (!term)
81002
- return { resized: false, error: "No terminal instance found" };
81003
- term.resize(c, r);
81004
- return { resized: true, cols: c, rows: r };
81005
- }, [cols, rows]);
81006
- return json(result);
81369
+ const result = await withTuiHealth(sid, async (page) => {
81370
+ return page.evaluate((args) => {
81371
+ const [c, r] = args;
81372
+ const term = window.term ?? window.terminal;
81373
+ if (!term)
81374
+ return { resized: false, error: "No terminal instance found" };
81375
+ term.resize(c, r);
81376
+ return { resized: true, cols: c, rows: r };
81377
+ }, [cols, rows]);
81378
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_resize" });
81379
+ return json(withMeta(sid, result));
81007
81380
  } catch (e) {
81381
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
81382
+ return err(e);
81008
81383
  return err(e);
81009
81384
  }
81010
81385
  });
81011
- server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.
81012
-
81013
- Use this to read what the terminal is currently displaying. For waiting until specific text appears, use browser_tui_wait_for_text instead.`, {
81386
+ server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.`, {
81014
81387
  session_id: exports_external2.string().optional(),
81015
81388
  start_row: exports_external2.number().optional().describe("First row to read (0-indexed, default: 0)"),
81016
- end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows.")
81017
- }, async ({ session_id, start_row, end_row }) => {
81389
+ end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows."),
81390
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81391
+ }, async ({ session_id, start_row, end_row, timeout_ms }) => {
81018
81392
  try {
81019
81393
  const sid = resolveSessionId(session_id);
81020
81394
  assertTuiSession(sid);
81021
- const page = getSessionPage(sid);
81022
- const result = await getTermText(page, start_row, end_row);
81023
- return json(result);
81395
+ const result = await withTuiHealth(sid, async (page, session) => {
81396
+ const state = await getTerminalState(page, session.method, timeout_ms);
81397
+ const filtered = filterRows(state.rows, start_row, end_row);
81398
+ return {
81399
+ ...filtered,
81400
+ row_count: state.row_count
81401
+ };
81402
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_get_text" });
81403
+ return json(withMeta(sid, result));
81024
81404
  } catch (e) {
81405
+ if (e.code === "TUI_TIMEOUT")
81406
+ return err(e);
81407
+ if (e.code === "TUI_UNHEALTHY")
81408
+ return err(e);
81025
81409
  return err(e);
81026
81410
  }
81027
81411
  });
81028
- server.tool("browser_tui_wait_for_text", `Wait for specific text to appear in the terminal output. Polls the terminal buffer until the text is found or timeout is reached.
81029
-
81030
- Use this after sending a command to wait for its output, or to wait for a TUI app to finish loading.`, {
81412
+ server.tool("browser_tui_wait_for_text", `Wait for specific text to appear in the terminal output. Polls until found or timeout.
81413
+ Returns stuck:true if the terminal became unresponsive during the wait.`, {
81031
81414
  session_id: exports_external2.string().optional(),
81032
81415
  text: exports_external2.string().describe("Text to wait for (substring match)"),
81033
81416
  timeout_ms: exports_external2.number().optional().default(30000).describe("Timeout in milliseconds (default: 30000)")
@@ -81035,38 +81418,37 @@ Use this after sending a command to wait for its output, or to wait for a TUI ap
81035
81418
  try {
81036
81419
  const sid = resolveSessionId(session_id);
81037
81420
  assertTuiSession(sid);
81038
- const page = getSessionPage(sid);
81039
- const start = Date.now();
81040
- while (Date.now() - start < timeout_ms) {
81041
- const result = await getTermText(page);
81042
- if (result.text.includes(text)) {
81043
- return json({ found: true, elapsed_ms: Date.now() - start, terminal_text: result.text });
81044
- }
81045
- await new Promise((r) => setTimeout(r, 250));
81046
- }
81047
- const finalText = await getTermText(page);
81048
- return json({ found: false, elapsed_ms: timeout_ms, terminal_text: finalText.text });
81421
+ const result = await withTuiHealth(sid, async (page, session) => {
81422
+ return waitForTerminalText(page, text, timeout_ms, session.method);
81423
+ }, { timeoutMs: timeout_ms + 5000, operationName: "browser_tui_wait_for_text" });
81424
+ return json(withMeta(sid, result));
81049
81425
  } catch (e) {
81426
+ if (e.code === "TUI_TIMEOUT")
81427
+ return err(e);
81428
+ if (e.code === "TUI_UNHEALTHY")
81429
+ return err(e);
81050
81430
  return err(e);
81051
81431
  }
81052
81432
  });
81053
81433
  server.tool("browser_tui_get_cursor", "Get the current cursor position (row and column) in the terminal.", {
81054
- session_id: exports_external2.string().optional()
81055
- }, async ({ session_id }) => {
81434
+ session_id: exports_external2.string().optional(),
81435
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81436
+ }, async ({ session_id, timeout_ms }) => {
81056
81437
  try {
81057
81438
  const sid = resolveSessionId(session_id);
81058
81439
  assertTuiSession(sid);
81059
- const page = getSessionPage(sid);
81060
- const cursor = await page.evaluate(() => {
81061
- const term = window.term ?? window.terminal;
81062
- if (!term?.buffer?.active)
81440
+ const result = await withTuiHealth(sid, async (page, session) => {
81441
+ const state = await getTerminalState(page, session.method, timeout_ms);
81442
+ if (state.cursor_row < 0 || state.cursor_col < 0)
81063
81443
  return null;
81064
- return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
81065
- });
81066
- if (!cursor)
81067
- return err(new Error("Could not read cursor position \u2014 no terminal instance"));
81068
- return json(cursor);
81444
+ return { row: state.cursor_row, col: state.cursor_col };
81445
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_get_cursor" });
81446
+ if (!result)
81447
+ return err(new Error("Could not read cursor \u2014 no terminal instance"));
81448
+ return json(withStableMeta(sid, result));
81069
81449
  } catch (e) {
81450
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
81451
+ return err(e);
81070
81452
  return err(e);
81071
81453
  }
81072
81454
  });
@@ -81077,140 +81459,135 @@ CONDITION SYNTAX:
81077
81459
  - "row N contains X" \u2014 row N (0-indexed) contains substring X
81078
81460
  - "cursor at R,C" \u2014 cursor is at row R, column C
81079
81461
  - "row_count > N" \u2014 total rows greater than N
81080
- - "row_count == N" \u2014 total rows equals N
81081
-
81082
- Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
81462
+ - "row_count == N" \u2014 total rows equals N`, {
81083
81463
  session_id: exports_external2.string().optional(),
81084
- condition: exports_external2.string().describe("Assertion condition(s), joined with AND")
81085
- }, async ({ session_id, condition }) => {
81464
+ condition: exports_external2.string().describe("Assertion condition(s), joined with AND"),
81465
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81466
+ }, async ({ session_id, condition, timeout_ms }) => {
81086
81467
  try {
81087
81468
  const sid = resolveSessionId(session_id);
81088
81469
  assertTuiSession(sid);
81089
- const page = getSessionPage(sid);
81090
- const termData = await getTermText(page);
81091
- const cursor = await page.evaluate(() => {
81092
- const term = window.term ?? window.terminal;
81093
- if (!term?.buffer?.active)
81094
- return { row: -1, col: -1 };
81095
- return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
81096
- });
81097
- const checks = [];
81098
- let allPassed = true;
81099
- for (const part of condition.split(/\s+AND\s+/i)) {
81100
- const trimmed = part.trim();
81101
- let result = false;
81102
- if (/^text\s+contains\s+/i.test(trimmed)) {
81103
- const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
81104
- result = termData.text.includes(needle);
81105
- } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
81106
- const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
81107
- if (match) {
81108
- const rowIdx = parseInt(match[1]);
81109
- const needle = match[2].replace(/^["']|["']$/g, "");
81110
- result = (termData.rows[rowIdx] ?? "").includes(needle);
81111
- }
81112
- } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
81113
- const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
81114
- if (match) {
81115
- result = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
81116
- }
81117
- } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
81118
- const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
81119
- if (match) {
81120
- const op = match[1];
81121
- const n = parseInt(match[2]);
81122
- const count = termData.row_count;
81123
- result = op === ">" ? count > n : op === ">=" ? count >= n : op === "<" ? count < n : op === "<=" ? count <= n : op === "==" ? count === n : count !== n;
81124
- }
81125
- }
81126
- checks.push({ assertion: trimmed, result });
81127
- if (!result)
81128
- allPassed = false;
81129
- }
81130
- return json({ passed: allPassed, checks, cursor, row_count: termData.row_count });
81470
+ const result = await withTuiHealth(sid, async (page, session) => {
81471
+ const state = await getTerminalState(page, session.method, timeout_ms);
81472
+ const termText = state.text;
81473
+ const cursor = { row: state.cursor_row, col: state.cursor_col };
81474
+ const checks = [];
81475
+ let allPassed = true;
81476
+ for (const part of condition.split(/\s+AND\s+/i)) {
81477
+ const trimmed = part.trim();
81478
+ let passed = false;
81479
+ if (/^text\s+contains\s+/i.test(trimmed)) {
81480
+ const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
81481
+ passed = termText.includes(needle);
81482
+ } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
81483
+ const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
81484
+ if (match) {
81485
+ const rowIdx = parseInt(match[1]);
81486
+ const needle = match[2].replace(/^["']|["']$/g, "");
81487
+ passed = (state.rows[rowIdx] ?? "").includes(needle);
81488
+ }
81489
+ } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
81490
+ const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
81491
+ if (match)
81492
+ passed = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
81493
+ } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
81494
+ const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
81495
+ if (match) {
81496
+ const op = match[1];
81497
+ const n = parseInt(match[2]);
81498
+ const cnt = state.row_count;
81499
+ passed = op === ">" ? cnt > n : op === ">=" ? cnt >= n : op === "<" ? cnt < n : op === "<=" ? cnt <= n : op === "==" ? cnt === n : cnt !== n;
81500
+ }
81501
+ }
81502
+ checks.push({ assertion: trimmed, result: passed });
81503
+ if (!passed)
81504
+ allPassed = false;
81505
+ }
81506
+ return { passed: allPassed, checks, cursor, row_count: state.row_count };
81507
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_assert" });
81508
+ return json(withMeta(sid, result));
81131
81509
  } catch (e) {
81510
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
81511
+ return err(e);
81132
81512
  return err(e);
81133
81513
  }
81134
81514
  });
81135
- server.tool("browser_tui_snapshot", "Capture a structured snapshot of the terminal buffer: all rows as an array, cursor position, dimensions, and theme. Useful for comparing terminal state before and after actions.", {
81136
- session_id: exports_external2.string().optional()
81137
- }, async ({ session_id }) => {
81515
+ server.tool("browser_tui_snapshot", "Capture a structured snapshot of the terminal buffer: all rows, row refs, cursor position, dimensions, and theme.", {
81516
+ session_id: exports_external2.string().optional(),
81517
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
81518
+ }, async ({ session_id, timeout_ms }) => {
81138
81519
  try {
81139
81520
  const sid = resolveSessionId(session_id);
81140
81521
  assertTuiSession(sid);
81141
- const page = getSessionPage(sid);
81142
- const snapshot = await page.evaluate(() => {
81143
- const term = window.term ?? window.terminal;
81144
- if (!term?.buffer?.active)
81145
- return null;
81146
- const buf = term.buffer.active;
81147
- const rows = [];
81148
- for (let i = 0;i < buf.length; i++) {
81149
- const line = buf.getLine(i);
81150
- if (line)
81151
- rows.push(line.translateToString(true));
81152
- }
81522
+ const result = await withTuiHealth(sid, async (page, session) => {
81523
+ const state = await getTerminalState(page, session.method, timeout_ms);
81153
81524
  return {
81154
- rows,
81155
- cols: term.cols,
81156
- total_rows: term.rows,
81157
- buffer_length: buf.length,
81158
- cursor_row: buf.cursorY,
81159
- cursor_col: buf.cursorX,
81160
- font_size: term.options?.fontSize,
81161
- theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
81525
+ rows: state.rows,
81526
+ refs: state.refs,
81527
+ cols: state.cols,
81528
+ total_rows: state.total_rows,
81529
+ buffer_length: state.buffer_length,
81530
+ cursor_row: state.cursor_row,
81531
+ cursor_col: state.cursor_col,
81532
+ font_size: state.font_size,
81533
+ theme: state.theme
81162
81534
  };
81163
- });
81164
- if (!snapshot)
81165
- return err(new Error("Could not capture snapshot \u2014 no terminal instance"));
81166
- return json(snapshot);
81535
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_snapshot" });
81536
+ return json(withStableMeta(sid, result));
81167
81537
  } catch (e) {
81538
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
81539
+ return err(e);
81168
81540
  return err(e);
81169
81541
  }
81170
81542
  });
81171
- server.tool("browser_tui_record_start", "Start recording the terminal session as an asciicast v2 file (asciinema-compatible). Polls the terminal buffer at an interval and captures changes.", {
81543
+ server.tool("browser_tui_record_start", "Start recording the terminal session as an asciicast v2 file.", {
81172
81544
  session_id: exports_external2.string().optional(),
81173
81545
  interval_ms: exports_external2.number().optional().default(500).describe("Polling interval in ms (default: 500)")
81174
81546
  }, async ({ session_id, interval_ms }) => {
81175
81547
  try {
81176
81548
  const sid = resolveSessionId(session_id);
81177
81549
  assertTuiSession(sid);
81178
- const page = getSessionPage(sid);
81179
81550
  if (activeRecordings2.has(sid)) {
81180
81551
  return err(new Error("Recording already active for this session. Stop it first with browser_tui_record_stop."));
81181
81552
  }
81182
- const dims = await page.evaluate(() => {
81183
- const term = window.term ?? window.terminal;
81184
- return term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
81185
- });
81186
- const initialText = (await getTermText(page)).text;
81553
+ const page = getSessionPage(sid);
81554
+ const session = getTuiSession(sid);
81555
+ const initialState = await getTerminalState(page, session.method);
81556
+ const dims = { cols: initialState.cols ?? 80, rows: initialState.total_rows || initialState.row_count || 24 };
81187
81557
  const recording = {
81188
81558
  sessionId: sid,
81189
81559
  startTime: Date.now(),
81190
81560
  cols: dims.cols,
81191
81561
  rows: dims.rows,
81192
81562
  events: [],
81193
- lastText: initialText,
81563
+ lastText: initialState.text,
81194
81564
  intervalId: setInterval(async () => {
81195
81565
  try {
81196
- const current = await getTermText(page);
81197
- if (current.text !== recording.lastText) {
81566
+ const currentPage = getSessionPage(sid);
81567
+ const currentSession = getTuiSession(sid);
81568
+ const state = await getTerminalState(currentPage, currentSession.method);
81569
+ if (state.text !== recording.lastText) {
81198
81570
  const elapsed = (Date.now() - recording.startTime) / 1000;
81199
- recording.events.push([elapsed, "o", current.text.slice(recording.lastText.length) || current.text]);
81200
- recording.lastText = current.text;
81571
+ recording.events.push([elapsed, "o", state.text.slice(recording.lastText.length) || state.text]);
81572
+ recording.lastText = state.text;
81201
81573
  }
81202
81574
  } catch {}
81203
81575
  }, interval_ms)
81204
81576
  };
81205
81577
  activeRecordings2.set(sid, recording);
81206
- return json({ recording: true, session_id: sid, interval_ms, cols: dims.cols, rows: dims.rows });
81578
+ return json({
81579
+ recording: true,
81580
+ session_id: sid,
81581
+ interval_ms,
81582
+ cols: dims.cols,
81583
+ rows: dims.rows,
81584
+ method: session.method
81585
+ });
81207
81586
  } catch (e) {
81208
81587
  return err(e);
81209
81588
  }
81210
81589
  });
81211
- server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON. Compatible with asciinema player.", {
81212
- session_id: exports_external2.string().optional()
81213
- }, async ({ session_id }) => {
81590
+ server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON.", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
81214
81591
  try {
81215
81592
  const sid = resolveSessionId(session_id);
81216
81593
  const recording = activeRecordings2.get(sid);
@@ -81228,25 +81605,45 @@ Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
81228
81605
  env: { TERM: "xterm-256color", SHELL: "/bin/bash" }
81229
81606
  };
81230
81607
  const lines = [JSON.stringify(header)];
81231
- for (const [time, type2, data] of recording.events) {
81608
+ for (const [time, type2, data] of recording.events)
81232
81609
  lines.push(JSON.stringify([time, type2, data]));
81233
- }
81234
81610
  const asciicast = lines.join(`
81235
81611
  `);
81236
81612
  return json({
81237
81613
  format: "asciicast_v2",
81238
81614
  duration_seconds: Math.round(duration * 10) / 10,
81239
81615
  event_count: recording.events.length,
81240
- asciicast
81616
+ asciicast,
81617
+ method: getTuiSession(sid).method
81618
+ });
81619
+ } catch (e) {
81620
+ return err(e);
81621
+ }
81622
+ });
81623
+ server.tool("browser_tui_health", `Health check for a TUI session. Returns healthy status, latency, reconnect count, and the active read method.
81624
+ Use this to verify a session is still responsive before running other tools.`, { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
81625
+ try {
81626
+ const sid = resolveSessionId(session_id);
81627
+ assertTuiSession(sid);
81628
+ const session = getTuiSession(sid);
81629
+ const health = await isTuiHealthy(session);
81630
+ return json({
81631
+ healthy: health.healthy,
81632
+ latency_ms: health.healthy ? health.latency_ms : null,
81633
+ reason: health.healthy ? null : health.reason,
81634
+ reconnect_count: session.reconnectCount,
81635
+ method: session.method
81241
81636
  });
81242
81637
  } catch (e) {
81243
81638
  return err(e);
81244
81639
  }
81245
81640
  });
81246
81641
  }
81247
- var KEY_MAP, activeRecordings2;
81642
+ var DEFAULT_TOOL_TIMEOUT_MS2 = 15000, RECONNECT_ON_STUCK = true, KEY_MAP, activeRecordings2;
81248
81643
  var init_tui2 = __esm(() => {
81249
81644
  init_helpers();
81645
+ init_tui();
81646
+ init_session();
81250
81647
  KEY_MAP = {
81251
81648
  "ctrl+c": "\x03",
81252
81649
  "ctrl+d": "\x04",
@@ -81368,32 +81765,71 @@ var init_snapshots = __esm(() => {
81368
81765
  var exports_server = {};
81369
81766
  import { join as join28 } from "path";
81370
81767
  import { existsSync as existsSync19 } from "fs";
81371
- function ok(data, status = 200) {
81768
+ function corsHeaders(origin) {
81769
+ const headers = {
81770
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
81771
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
81772
+ };
81773
+ if (origin) {
81774
+ if (!API_KEY && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
81775
+ headers["Access-Control-Allow-Origin"] = ALLOWED_ORIGIN ?? "http://localhost:3000";
81776
+ } else {
81777
+ headers["Access-Control-Allow-Origin"] = origin;
81778
+ }
81779
+ }
81780
+ return headers;
81781
+ }
81782
+ function authenticate(req) {
81783
+ if (!API_KEY)
81784
+ return null;
81785
+ const header = req.headers.get("Authorization") ?? "";
81786
+ const token = header.startsWith("Bearer ") ? header.slice(7) : "";
81787
+ if (token !== API_KEY) {
81788
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
81789
+ status: 401,
81790
+ headers: { "Content-Type": "application/json" }
81791
+ });
81792
+ }
81793
+ return null;
81794
+ }
81795
+ async function safeJson(req) {
81796
+ try {
81797
+ const contentType = req.headers.get("content-type") ?? "";
81798
+ if (!contentType.includes("application/json")) {
81799
+ return { error: badRequest("Content-Type must be application/json") };
81800
+ }
81801
+ const body = await req.json();
81802
+ return { body };
81803
+ } catch {
81804
+ return { error: badRequest("Invalid or missing JSON body") };
81805
+ }
81806
+ }
81807
+ function ok(data, status = 200, extraHeaders) {
81372
81808
  return new Response(JSON.stringify(data), {
81373
81809
  status,
81374
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
81810
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
81375
81811
  });
81376
81812
  }
81377
- function notFound(msg) {
81813
+ function notFound(msg, extraHeaders) {
81378
81814
  return new Response(JSON.stringify({ error: msg }), {
81379
81815
  status: 404,
81380
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
81816
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
81381
81817
  });
81382
81818
  }
81383
- function badRequest(msg) {
81819
+ function badRequest(msg, extraHeaders) {
81384
81820
  return new Response(JSON.stringify({ error: msg }), {
81385
81821
  status: 400,
81386
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
81822
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
81387
81823
  });
81388
81824
  }
81389
- function serverError(e) {
81825
+ function serverError(e, extraHeaders) {
81390
81826
  const msg = e instanceof Error ? e.message : String(e);
81391
81827
  return new Response(JSON.stringify({ error: msg }), {
81392
81828
  status: 500,
81393
- headers: { "Content-Type": "application/json", ...CORS_HEADERS }
81829
+ headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
81394
81830
  });
81395
81831
  }
81396
- var PORT, startTime, CORS_HEADERS, networkCleanup, consoleCleanup, harCaptures2, server2;
81832
+ var PORT, API_KEY, ALLOWED_ORIGIN, startTime, networkCleanup, consoleCleanup, harCaptures2, server2;
81397
81833
  var init_server = __esm(() => {
81398
81834
  init_session();
81399
81835
  init_actions();
@@ -81412,12 +81848,9 @@ var init_server = __esm(() => {
81412
81848
  init_downloads();
81413
81849
  init_gallery_diff();
81414
81850
  PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
81851
+ API_KEY = process.env["BROWSER_API_KEY"] ?? null;
81852
+ ALLOWED_ORIGIN = process.env["BROWSER_ALLOWED_ORIGIN"] ?? (API_KEY ? null : "http://localhost:3000");
81415
81853
  startTime = Date.now();
81416
- CORS_HEADERS = {
81417
- "Access-Control-Allow-Origin": "*",
81418
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
81419
- "Access-Control-Allow-Headers": "Content-Type"
81420
- };
81421
81854
  networkCleanup = new Map;
81422
81855
  consoleCleanup = new Map;
81423
81856
  harCaptures2 = new Map;
@@ -81427,8 +81860,15 @@ var init_server = __esm(() => {
81427
81860
  const url = new URL(req.url);
81428
81861
  const path = url.pathname;
81429
81862
  const method = req.method;
81863
+ const origin = req.headers.get("Origin") ?? undefined;
81864
+ const headers = corsHeaders(origin ?? null);
81430
81865
  if (method === "OPTIONS") {
81431
- return new Response(null, { status: 204, headers: CORS_HEADERS });
81866
+ return new Response(null, { status: 204, headers });
81867
+ }
81868
+ if (!path.startsWith("/dashboard") && path !== "/health") {
81869
+ const authError = authenticate(req);
81870
+ if (authError)
81871
+ return authError;
81432
81872
  }
81433
81873
  try {
81434
81874
  if (path === "/health" && method === "GET") {
@@ -81445,7 +81885,10 @@ var init_server = __esm(() => {
81445
81885
  return ok({ sessions: listSessions2(status ? { status, projectId } : { projectId }) });
81446
81886
  }
81447
81887
  if (path === "/api/sessions" && method === "POST") {
81448
- const body = await req.json();
81888
+ const parsed = await safeJson(req);
81889
+ if ("error" in parsed)
81890
+ return parsed.error;
81891
+ const body = parsed.body;
81449
81892
  const { session } = await createSession2({
81450
81893
  engine: body.engine ?? "auto",
81451
81894
  projectId: body.project_id,
@@ -81467,25 +81910,36 @@ var init_server = __esm(() => {
81467
81910
  return ok({ session });
81468
81911
  }
81469
81912
  if (path === "/api/navigate" && method === "POST") {
81470
- const body = await req.json();
81913
+ const parsed = await safeJson(req);
81914
+ if ("error" in parsed)
81915
+ return parsed.error;
81916
+ const body = parsed.body;
81471
81917
  if (!body.session_id || !body.url)
81472
- return badRequest("session_id and url required");
81473
- const page = getSessionPage(body.session_id);
81474
- await navigate(page, body.url);
81475
- return ok({ url: body.url, title: await page.title(), current_url: page.url() });
81918
+ return badRequest("session_id and url required", headers);
81919
+ const sessionId = body.session_id;
81920
+ const url2 = body.url;
81921
+ const page = getSessionPage(sessionId);
81922
+ await navigate(page, url2);
81923
+ return ok({ url: url2, title: await page.title(), current_url: page.url() });
81476
81924
  }
81477
81925
  if (path === "/api/extract" && method === "POST") {
81478
- const body = await req.json();
81926
+ const parsed = await safeJson(req);
81927
+ if ("error" in parsed)
81928
+ return parsed.error;
81929
+ const body = parsed.body;
81479
81930
  if (!body.session_id)
81480
- return badRequest("session_id required");
81931
+ return badRequest("session_id required", headers);
81481
81932
  const page = getSessionPage(body.session_id);
81482
81933
  const result = await extract(page, { format: body.format, selector: body.selector });
81483
81934
  return ok(result);
81484
81935
  }
81485
81936
  if (path === "/api/screenshot" && method === "POST") {
81486
- const body = await req.json();
81937
+ const parsed = await safeJson(req);
81938
+ if ("error" in parsed)
81939
+ return parsed.error;
81940
+ const body = parsed.body;
81487
81941
  if (!body.session_id)
81488
- return badRequest("session_id required");
81942
+ return badRequest("session_id required", headers);
81489
81943
  const page = getSessionPage(body.session_id);
81490
81944
  const result = await takeScreenshot(page, { selector: body.selector, fullPage: body.full_page });
81491
81945
  return ok(result);
@@ -81522,16 +81976,22 @@ var init_server = __esm(() => {
81522
81976
  return ok({ metrics: await getPerformanceMetrics(page) });
81523
81977
  }
81524
81978
  if (path === "/api/har/start" && method === "POST") {
81525
- const body = await req.json();
81979
+ const parsed = await safeJson(req);
81980
+ if ("error" in parsed)
81981
+ return parsed.error;
81982
+ const body = parsed.body;
81526
81983
  const page = getSessionPage(body.session_id);
81527
81984
  harCaptures2.set(body.session_id, startHAR(page));
81528
81985
  return ok({ started: true });
81529
81986
  }
81530
81987
  if (path === "/api/har/stop" && method === "POST") {
81531
- const body = await req.json();
81988
+ const parsed = await safeJson(req);
81989
+ if ("error" in parsed)
81990
+ return parsed.error;
81991
+ const body = parsed.body;
81532
81992
  const capture = harCaptures2.get(body.session_id);
81533
81993
  if (!capture)
81534
- return notFound("No active HAR capture");
81994
+ return notFound("No active HAR capture", headers);
81535
81995
  const har = capture.stop();
81536
81996
  harCaptures2.delete(body.session_id);
81537
81997
  return ok({ har });
@@ -81540,8 +82000,11 @@ var init_server = __esm(() => {
81540
82000
  return ok({ recordings: listRecordings(url.searchParams.get("project_id") ?? undefined) });
81541
82001
  }
81542
82002
  if (path.match(/^\/api\/recordings\/([^/]+)\/replay$/) && method === "POST") {
82003
+ const parsed = await safeJson(req);
82004
+ if ("error" in parsed)
82005
+ return parsed.error;
82006
+ const body = parsed.body;
81543
82007
  const id = path.split("/")[3];
81544
- const body = await req.json();
81545
82008
  const page = getSessionPage(body.session_id);
81546
82009
  const result = await replayRecording(id, page);
81547
82010
  return ok(result);
@@ -81553,9 +82016,12 @@ var init_server = __esm(() => {
81553
82016
  return ok({ deleted: id });
81554
82017
  }
81555
82018
  if (path === "/api/crawl" && method === "POST") {
81556
- const body = await req.json();
82019
+ const parsed = await safeJson(req);
82020
+ if ("error" in parsed)
82021
+ return parsed.error;
82022
+ const body = parsed.body;
81557
82023
  if (!body.url)
81558
- return badRequest("url required");
82024
+ return badRequest("url required", headers);
81559
82025
  const result = await crawl(body.url, {
81560
82026
  maxDepth: body.max_depth ?? 2,
81561
82027
  maxPages: body.max_pages ?? 50,
@@ -81567,9 +82033,12 @@ var init_server = __esm(() => {
81567
82033
  return ok({ agents: listAgents(url.searchParams.get("project_id") ?? undefined) });
81568
82034
  }
81569
82035
  if (path === "/api/agents" && method === "POST") {
81570
- const body = await req.json();
82036
+ const parsed = await safeJson(req);
82037
+ if ("error" in parsed)
82038
+ return parsed.error;
82039
+ const body = parsed.body;
81571
82040
  if (!body.name)
81572
- return badRequest("name required");
82041
+ return badRequest("name required", headers);
81573
82042
  const agent = registerAgent2(body.name, { description: body.description, projectId: body.project_id, sessionId: body.session_id, workingDir: body.working_dir });
81574
82043
  return ok({ agent }, 201);
81575
82044
  }
@@ -81588,9 +82057,12 @@ var init_server = __esm(() => {
81588
82057
  return ok({ projects: listProjects() });
81589
82058
  }
81590
82059
  if (path === "/api/projects" && method === "POST") {
81591
- const body = await req.json();
82060
+ const parsed = await safeJson(req);
82061
+ if ("error" in parsed)
82062
+ return parsed.error;
82063
+ const body = parsed.body;
81592
82064
  if (!body.name || !body.path)
81593
- return badRequest("name and path required");
82065
+ return badRequest("name and path required", headers);
81594
82066
  const project = ensureProject(body.name, body.path, body.description);
81595
82067
  return ok({ project }, 201);
81596
82068
  }
@@ -81606,21 +82078,30 @@ var init_server = __esm(() => {
81606
82078
  return ok(getGalleryStats(url.searchParams.get("project_id") ?? undefined));
81607
82079
  }
81608
82080
  if (path === "/api/gallery/diff" && method === "POST") {
81609
- const body = await req.json();
82081
+ const parsed = await safeJson(req);
82082
+ if ("error" in parsed)
82083
+ return parsed.error;
82084
+ const body = parsed.body;
81610
82085
  const e1 = getEntry(body.id1);
81611
82086
  const e2 = getEntry(body.id2);
81612
82087
  if (!e1 || !e2)
81613
- return notFound("Gallery entry not found");
82088
+ return notFound("Gallery entry not found", headers);
81614
82089
  return ok(await diffImages(e1.path, e2.path));
81615
82090
  }
81616
82091
  if (path.match(/^\/api\/gallery\/([^/]+)\/tag$/) && method === "POST") {
82092
+ const parsed = await safeJson(req);
82093
+ if ("error" in parsed)
82094
+ return parsed.error;
82095
+ const body = parsed.body;
81617
82096
  const id = path.split("/")[3];
81618
- const body = await req.json();
81619
82097
  return ok({ entry: tagEntry(id, body.tag) });
81620
82098
  }
81621
82099
  if (path.match(/^\/api\/gallery\/([^/]+)\/favorite$/) && method === "PUT") {
82100
+ const parsed = await safeJson(req);
82101
+ if ("error" in parsed)
82102
+ return parsed.error;
82103
+ const body = parsed.body;
81622
82104
  const id = path.split("/")[3];
81623
- const body = await req.json();
81624
82105
  return ok({ entry: favoriteEntry(id, body.favorited) });
81625
82106
  }
81626
82107
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
@@ -81628,14 +82109,14 @@ var init_server = __esm(() => {
81628
82109
  const entry = getEntry(id);
81629
82110
  if (!entry?.thumbnail_path || !existsSync19(entry.thumbnail_path))
81630
82111
  return notFound("Thumbnail not found");
81631
- return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
82112
+ return new Response(Bun.file(entry.thumbnail_path), { headers: { ...headers } });
81632
82113
  }
81633
82114
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
81634
82115
  const id = path.split("/")[3];
81635
82116
  const entry = getEntry(id);
81636
82117
  if (!entry?.path || !existsSync19(entry.path))
81637
82118
  return notFound("Image not found");
81638
- return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
82119
+ return new Response(Bun.file(entry.path), { headers: { ...headers } });
81639
82120
  }
81640
82121
  if (path.match(/^\/api\/gallery\/([^/]+)$/) && method === "DELETE") {
81641
82122
  const id = path.split("/")[3];
@@ -81663,7 +82144,7 @@ var init_server = __esm(() => {
81663
82144
  const file = getDownload(id);
81664
82145
  if (!file || !existsSync19(file.path))
81665
82146
  return notFound("Download not found");
81666
- return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
82147
+ return new Response(Bun.file(file.path), { headers: { ...headers } });
81667
82148
  }
81668
82149
  if (path.match(/^\/api\/downloads\/([^/]+)$/) && method === "DELETE") {
81669
82150
  const id = path.split("/")[3];
@@ -81671,15 +82152,21 @@ var init_server = __esm(() => {
81671
82152
  }
81672
82153
  const dashboardDist = join28(import.meta.dir, "../../dashboard/dist");
81673
82154
  if (existsSync19(dashboardDist)) {
81674
- const filePath = path === "/" ? join28(dashboardDist, "index.html") : join28(dashboardDist, path);
82155
+ const cleanPath = path.replace(/^\//, "");
82156
+ if (cleanPath.includes("..") || cleanPath.startsWith("/"))
82157
+ return notFound("Not found", headers);
82158
+ const filePath = path === "/" ? join28(dashboardDist, "index.html") : join28(dashboardDist, cleanPath);
82159
+ const resolved = await Bun.file(filePath).arrayBuffer().then(() => join28(dashboardDist, cleanPath)) || "";
82160
+ if (!resolved.startsWith(dashboardDist))
82161
+ return notFound("Not found", headers);
81675
82162
  if (existsSync19(filePath)) {
81676
- return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
82163
+ return new Response(Bun.file(filePath), { headers });
81677
82164
  }
81678
- return new Response(Bun.file(join28(dashboardDist, "index.html")), { headers: CORS_HEADERS });
82165
+ return new Response(Bun.file(join28(dashboardDist, "index.html")), { headers });
81679
82166
  }
81680
82167
  if (path === "/" || path === "") {
81681
82168
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
81682
- headers: { "Content-Type": "text/plain", ...CORS_HEADERS }
82169
+ headers: { "Content-Type": "text/plain", ...headers }
81683
82170
  });
81684
82171
  }
81685
82172
  return notFound(`Route not found: ${method} ${path}`);
@@ -82142,6 +82629,7 @@ init_projects();
82142
82629
  init_recorder();
82143
82630
  init_recordings();
82144
82631
  init_lightpanda();
82632
+ init_types2();
82145
82633
  import chalk4 from "chalk";
82146
82634
  function register13(program2) {
82147
82635
  const recordCmd = program2.command("record").description("Manage action recordings");
@@ -82219,9 +82707,10 @@ function register13(program2) {
82219
82707
  console.log(chalk4.blue(` Page: ${title} (${url})`));
82220
82708
  }
82221
82709
  });
82222
- program2.command("login <url>").description("Login to a site: detect form, fill credentials from secrets, save auth state").option("--email <email>", "Email to login with").option("--save-as <name>", "Name to save storage state as").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
82223
- const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
82710
+ program2.command("login <url>").description("Login to a site: detect form, fill credentials from secrets, save auth state").option("--email <email>", "Email to login with").option("--password <password>", "Password to login with").option("--save-as <name>", "Name to save storage state as").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
82711
+ const { session, page } = await createSession2({ engine: opts.engine, useCase: "auth_flow" /* AUTH_FLOW */, headless: !opts.headed });
82224
82712
  await navigate(page, url);
82713
+ await new Promise((r) => setTimeout(r, 2000));
82225
82714
  const formInfo = await page.evaluate(() => {
82226
82715
  const emailInput = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[autocomplete="email"], input[autocomplete="username"]');
82227
82716
  const passwordInput = document.querySelector('input[type="password"]');
@@ -82243,15 +82732,15 @@ function register13(program2) {
82243
82732
  console.log(chalk4.gray(` Submit button: ${formInfo.hasSubmitButton ? "\u2713" : "\u2717"}`));
82244
82733
  }
82245
82734
  let email = opts.email;
82246
- let password;
82247
- if (!email) {
82735
+ let password = opts.password;
82736
+ if (!email || !password) {
82248
82737
  try {
82249
82738
  const { getCredentials: getCredentials2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
82250
82739
  const hostname2 = new URL(url).hostname;
82251
82740
  const creds = await getCredentials2(hostname2);
82252
82741
  if (creds) {
82253
- email = creds.email ?? creds.username;
82254
- password = creds.password;
82742
+ email = email ?? creds.email ?? creds.username;
82743
+ password = password ?? creds.password;
82255
82744
  if (!opts.json)
82256
82745
  console.log(chalk4.blue(` Credentials found for ${hostname2}`));
82257
82746
  }