@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/mcp/index.js CHANGED
@@ -11286,6 +11286,340 @@ var init_bun_webview = __esm(() => {
11286
11286
 
11287
11287
  // src/engines/tui.ts
11288
11288
  import { execSync as execSync2, spawn as spawn2 } from "child_process";
11289
+ function normalizeRowText(text) {
11290
+ return text.replace(/\u00a0/g, " ").replace(/\s+$/g, "");
11291
+ }
11292
+ function buildRowRefs(rows, method, totalRows, rowCount) {
11293
+ const refs = {};
11294
+ const firstVisibleRow = method === "buffer" ? Math.max(0, rowCount - totalRows) : 0;
11295
+ rows.forEach((text, index) => {
11296
+ refs[`@r${index}`] = {
11297
+ row: index,
11298
+ text,
11299
+ visible: method === "dom" ? true : index >= firstVisibleRow,
11300
+ selector: method === "dom" ? `#takumi-tui-dom-root [data-row="${index}"]` : undefined
11301
+ };
11302
+ });
11303
+ return refs;
11304
+ }
11305
+ async function configureDomRenderer(page, options) {
11306
+ await page.evaluate((opts) => {
11307
+ const runtimeKey = "__takumiTuiDomRenderer";
11308
+ const rootId = "takumi-tui-dom-root";
11309
+ const styleId = "takumi-tui-dom-style";
11310
+ const win = window;
11311
+ const ensureStyle = () => {
11312
+ let style = document.getElementById(styleId);
11313
+ if (!style) {
11314
+ style = document.createElement("style");
11315
+ style.id = styleId;
11316
+ document.head.appendChild(style);
11317
+ }
11318
+ style.textContent = `
11319
+ #${rootId} {
11320
+ position: absolute;
11321
+ inset: 0;
11322
+ overflow: hidden;
11323
+ display: flex;
11324
+ flex-direction: column;
11325
+ white-space: pre;
11326
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
11327
+ line-height: 1.2;
11328
+ background: var(--takumi-tui-bg, #1e1e1e);
11329
+ color: var(--takumi-tui-fg, #d4d4d4);
11330
+ z-index: 4;
11331
+ pointer-events: none;
11332
+ user-select: text;
11333
+ }
11334
+ #${rootId}[data-active="0"] {
11335
+ display: none;
11336
+ }
11337
+ #${rootId} .takumi-tui-dom-row {
11338
+ display: flex;
11339
+ min-height: 1.2em;
11340
+ }
11341
+ #${rootId} .takumi-tui-dom-cell {
11342
+ display: inline-flex;
11343
+ align-items: center;
11344
+ justify-content: center;
11345
+ min-width: 0.62em;
11346
+ height: 1.2em;
11347
+ }
11348
+ #${rootId} .takumi-tui-dom-cell[data-cursor="true"] {
11349
+ outline: 1px solid currentColor;
11350
+ outline-offset: -1px;
11351
+ }
11352
+ body[data-takumi-dom-render="1"] .xterm-rows,
11353
+ body[data-takumi-dom-render="1"] .xterm-text-layer,
11354
+ body[data-takumi-dom-render="1"] .xterm-cursor-layer,
11355
+ body[data-takumi-dom-render="1"] .xterm-selection-layer {
11356
+ opacity: 0 !important;
11357
+ }
11358
+ `;
11359
+ };
11360
+ const ensureRoot = () => {
11361
+ let root = document.getElementById(rootId);
11362
+ if (!root) {
11363
+ root = document.createElement("div");
11364
+ root.id = rootId;
11365
+ root.setAttribute("role", "grid");
11366
+ root.setAttribute("aria-label", "Terminal DOM renderer");
11367
+ const host = document.getElementById("terminal-container") ?? document.querySelector(".xterm") ?? document.body;
11368
+ if (getComputedStyle(host).position === "static") {
11369
+ host.style.position = "relative";
11370
+ }
11371
+ host.appendChild(root);
11372
+ }
11373
+ root.style.setProperty("--takumi-tui-bg", opts.theme === "light" ? "#ffffff" : "#1e1e1e");
11374
+ root.style.setProperty("--takumi-tui-fg", opts.theme === "light" ? "#1e1e1e" : "#d4d4d4");
11375
+ root.style.fontSize = `${opts.fontSize ?? 14}px`;
11376
+ root.dataset.active = opts.active ? "1" : "0";
11377
+ if (opts.active)
11378
+ root.removeAttribute("aria-hidden");
11379
+ else
11380
+ root.setAttribute("aria-hidden", "true");
11381
+ document.body.dataset.takumiDomRender = opts.active ? "1" : "0";
11382
+ return root;
11383
+ };
11384
+ const readCellChars = (line, col) => {
11385
+ try {
11386
+ const cell = typeof line?.getCell === "function" ? line.getCell(col) : null;
11387
+ const chars = typeof cell?.getChars === "function" ? cell.getChars() : "";
11388
+ if (chars)
11389
+ return chars;
11390
+ } catch {}
11391
+ try {
11392
+ const text = typeof line?.translateToString === "function" ? line.translateToString(false, col, col + 1) : "";
11393
+ if (text)
11394
+ return text;
11395
+ } catch {}
11396
+ return " ";
11397
+ };
11398
+ const buildState = (activeOnly) => {
11399
+ const term = win.term ?? win.terminal;
11400
+ if (!term?.buffer?.active) {
11401
+ return {
11402
+ text: "",
11403
+ rows: [],
11404
+ row_count: 0,
11405
+ cols: null,
11406
+ total_rows: 0,
11407
+ buffer_length: null,
11408
+ cursor_row: -1,
11409
+ cursor_col: -1,
11410
+ font_size: null,
11411
+ theme: opts.theme
11412
+ };
11413
+ }
11414
+ const buf = term.buffer.active;
11415
+ const rows = [];
11416
+ const root = ensureRoot();
11417
+ const fragment = document.createDocumentFragment();
11418
+ for (let row = 0;row < buf.length; row++) {
11419
+ const line = buf.getLine(row);
11420
+ if (!line)
11421
+ continue;
11422
+ const rowEl = document.createElement("div");
11423
+ rowEl.className = "takumi-tui-dom-row";
11424
+ rowEl.setAttribute("role", "row");
11425
+ rowEl.dataset.row = String(row);
11426
+ rowEl.setAttribute("aria-rowindex", String(row + 1));
11427
+ let rowText = "";
11428
+ for (let col = 0;col < term.cols; col++) {
11429
+ const char = readCellChars(line, col) || " ";
11430
+ rowText += char;
11431
+ const cellEl = document.createElement("span");
11432
+ cellEl.className = "takumi-tui-dom-cell";
11433
+ cellEl.setAttribute("role", "gridcell");
11434
+ cellEl.dataset.row = String(row);
11435
+ cellEl.dataset.col = String(col);
11436
+ cellEl.setAttribute("aria-colindex", String(col + 1));
11437
+ cellEl.textContent = char;
11438
+ if (buf.cursorY === row && buf.cursorX === col) {
11439
+ cellEl.dataset.cursor = "true";
11440
+ }
11441
+ rowEl.appendChild(cellEl);
11442
+ }
11443
+ rows.push(rowText.replace(/\s+$/g, ""));
11444
+ rowEl.setAttribute("aria-label", rows[rows.length - 1] || " ");
11445
+ fragment.appendChild(rowEl);
11446
+ }
11447
+ root.replaceChildren(fragment);
11448
+ root.setAttribute("aria-rowcount", String(rows.length));
11449
+ root.dataset.method = "dom";
11450
+ return {
11451
+ text: rows.join(`
11452
+ `).trimEnd(),
11453
+ rows,
11454
+ row_count: rows.length,
11455
+ cols: term.cols,
11456
+ total_rows: term.rows,
11457
+ buffer_length: buf.length,
11458
+ cursor_row: buf.cursorY,
11459
+ cursor_col: buf.cursorX,
11460
+ font_size: term.options?.fontSize ?? null,
11461
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
11462
+ };
11463
+ };
11464
+ ensureStyle();
11465
+ ensureRoot();
11466
+ if (!win[runtimeKey]) {
11467
+ win[runtimeKey] = {
11468
+ sync: () => buildState(false),
11469
+ activate: (active) => {
11470
+ const root = ensureRoot();
11471
+ root.dataset.active = active ? "1" : "0";
11472
+ if (active)
11473
+ root.removeAttribute("aria-hidden");
11474
+ else
11475
+ root.setAttribute("aria-hidden", "true");
11476
+ document.body.dataset.takumiDomRender = active ? "1" : "0";
11477
+ }
11478
+ };
11479
+ const intervalId = window.setInterval(() => {
11480
+ try {
11481
+ win[runtimeKey]?.sync?.();
11482
+ } catch {}
11483
+ }, 50);
11484
+ win[runtimeKey].intervalId = intervalId;
11485
+ }
11486
+ win[runtimeKey].activate(opts.active);
11487
+ win[runtimeKey].sync();
11488
+ }, options);
11489
+ }
11490
+ async function destroyDomRenderer(page) {
11491
+ await page.evaluate(() => {
11492
+ const runtimeKey = "__takumiTuiDomRenderer";
11493
+ const win = window;
11494
+ if (win[runtimeKey]?.intervalId) {
11495
+ clearInterval(win[runtimeKey].intervalId);
11496
+ }
11497
+ delete win[runtimeKey];
11498
+ document.getElementById("takumi-tui-dom-root")?.remove();
11499
+ document.getElementById("takumi-tui-dom-style")?.remove();
11500
+ delete document.body.dataset.takumiDomRender;
11501
+ }).catch(() => {});
11502
+ }
11503
+ async function readDomMirrorState(page) {
11504
+ return page.evaluate(() => {
11505
+ const runtime = window.__takumiTuiDomRenderer;
11506
+ if (runtime?.sync)
11507
+ return runtime.sync();
11508
+ const rowEls = Array.from(document.querySelectorAll("#takumi-tui-dom-root [data-row]"));
11509
+ const rows = rowEls.map((row) => row.getAttribute("aria-label") ?? row.textContent ?? "");
11510
+ const term = window.term ?? window.terminal;
11511
+ const active = term?.buffer?.active;
11512
+ return {
11513
+ text: rows.join(`
11514
+ `).trimEnd(),
11515
+ rows,
11516
+ row_count: rows.length,
11517
+ cols: term?.cols ?? null,
11518
+ total_rows: term?.rows ?? rows.length,
11519
+ buffer_length: active?.length ?? rows.length,
11520
+ cursor_row: active?.cursorY ?? -1,
11521
+ cursor_col: active?.cursorX ?? -1,
11522
+ font_size: term?.options?.fontSize ?? null,
11523
+ theme: term?.options?.theme?.background === "#ffffff" ? "light" : "dark"
11524
+ };
11525
+ });
11526
+ }
11527
+ function isDomMethod(method) {
11528
+ return method === "dom";
11529
+ }
11530
+ async function withTimeout(label, operation, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
11531
+ let timedOut = false;
11532
+ const timer = setTimeout(() => {
11533
+ timedOut = true;
11534
+ }, timeoutMs);
11535
+ try {
11536
+ return await operation();
11537
+ } catch (err) {
11538
+ if (timedOut) {
11539
+ 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");
11540
+ }
11541
+ throw err;
11542
+ } finally {
11543
+ clearTimeout(timer);
11544
+ }
11545
+ }
11546
+ async function isTuiHealthy(session) {
11547
+ const start = Date.now();
11548
+ try {
11549
+ await Promise.race([
11550
+ session.page.evaluate(() => {
11551
+ const term = window.term ?? window.terminal;
11552
+ if (!term)
11553
+ return false;
11554
+ if (!term.buffer?.active)
11555
+ return false;
11556
+ return true;
11557
+ }),
11558
+ new Promise((_, reject) => setTimeout(() => reject(new Error("health check timeout")), HEALTH_CHECK_TIMEOUT_MS))
11559
+ ]);
11560
+ const latency = Date.now() - start;
11561
+ return { healthy: true, latency_ms: latency };
11562
+ } catch (err) {
11563
+ return { healthy: false, reason: err?.message ?? "unreachable" };
11564
+ }
11565
+ }
11566
+ async function reconnectTui(session, command, options = {}) {
11567
+ const port = session.port;
11568
+ try {
11569
+ session.ttydProcess.kill("SIGTERM");
11570
+ } catch {}
11571
+ try {
11572
+ await session.page.close();
11573
+ } catch {}
11574
+ try {
11575
+ await session.browser.close();
11576
+ } catch {}
11577
+ const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
11578
+ ttydProcess.on("error", (err) => {
11579
+ console.error(`[tui] reconnect ttyd error: ${err.message}`);
11580
+ });
11581
+ await waitForTtyd(port);
11582
+ const viewport = options.viewport ?? { width: 1280, height: 720 };
11583
+ const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
11584
+ const page = await getPage(browser, { viewport });
11585
+ await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
11586
+ await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
11587
+ let resolvedTheme = "dark";
11588
+ const req = options.theme ?? "dark";
11589
+ if (req === "light" || req === "dark") {
11590
+ resolvedTheme = req;
11591
+ } else {
11592
+ try {
11593
+ const r = execSync2("defaults read -g AppleInterfaceStyle 2>/dev/null", { encoding: "utf8" }).trim();
11594
+ resolvedTheme = r === "Dark" ? "dark" : "light";
11595
+ } catch {
11596
+ resolvedTheme = "light";
11597
+ }
11598
+ }
11599
+ const themeColors = THEMES[resolvedTheme];
11600
+ await page.evaluate((theme) => {
11601
+ const term = window.term ?? window.terminal;
11602
+ if (term?.options)
11603
+ term.options.theme = theme;
11604
+ document.body.style.backgroundColor = theme.background;
11605
+ }, themeColors);
11606
+ const method = options.method ?? session.method;
11607
+ await configureDomRenderer(page, {
11608
+ active: isDomMethod(method),
11609
+ theme: resolvedTheme,
11610
+ fontSize: options.fontSize
11611
+ });
11612
+ return {
11613
+ ttydProcess,
11614
+ port,
11615
+ browser,
11616
+ page,
11617
+ theme: resolvedTheme,
11618
+ method,
11619
+ lastHealthCheck: Date.now(),
11620
+ reconnectCount: session.reconnectCount + 1
11621
+ };
11622
+ }
11289
11623
  function isTuiAvailable() {
11290
11624
  try {
11291
11625
  execSync2("which ttyd", { stdio: "ignore" });
@@ -11298,7 +11632,7 @@ async function findAvailablePort(startPort) {
11298
11632
  let port = startPort;
11299
11633
  for (let i = 0;i < 100; i++) {
11300
11634
  try {
11301
- const resp = await fetch(`http://localhost:${port}`);
11635
+ await fetch(`http://localhost:${port}`);
11302
11636
  port++;
11303
11637
  } catch {
11304
11638
  return port;
@@ -11324,24 +11658,16 @@ async function launchTui(command, options = {}) {
11324
11658
  }
11325
11659
  const port = await findAvailablePort(nextPort);
11326
11660
  nextPort = port + 1;
11327
- const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], {
11328
- stdio: "ignore",
11329
- detached: false
11330
- });
11661
+ const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
11331
11662
  ttydProcess.on("error", (err) => {
11332
11663
  console.error(`[tui] ttyd process error: ${err.message}`);
11333
11664
  });
11334
11665
  try {
11335
11666
  await waitForTtyd(port);
11336
11667
  const viewport = options.viewport ?? { width: 1280, height: 720 };
11337
- const browser = await launchPlaywright({
11338
- headless: options.headless ?? true,
11339
- viewport
11340
- });
11668
+ const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
11341
11669
  const page = await getPage(browser, { viewport });
11342
- await page.goto(`http://localhost:${port}`, {
11343
- waitUntil: "domcontentloaded"
11344
- });
11670
+ await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
11345
11671
  await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
11346
11672
  let resolvedTheme = "dark";
11347
11673
  const requestedTheme = options.theme ?? "system";
@@ -11360,16 +11686,15 @@ async function launchTui(command, options = {}) {
11360
11686
  const themeColors = THEMES[resolvedTheme];
11361
11687
  await page.evaluate((theme) => {
11362
11688
  const term = window.term ?? window.terminal;
11363
- if (term?.options) {
11689
+ if (term?.options)
11364
11690
  term.options.theme = theme;
11365
- }
11366
11691
  document.body.style.backgroundColor = theme.background;
11367
11692
  const container = document.getElementById("terminal-container");
11368
11693
  if (container)
11369
11694
  container.style.backgroundColor = theme.background;
11370
- const viewport2 = document.querySelector(".xterm-viewport");
11371
- if (viewport2)
11372
- viewport2.style.backgroundColor = theme.background;
11695
+ const vp = document.querySelector(".xterm-viewport");
11696
+ if (vp)
11697
+ vp.style.backgroundColor = theme.background;
11373
11698
  }, themeColors);
11374
11699
  if (options.fontSize) {
11375
11700
  await page.evaluate((size) => {
@@ -11378,13 +11703,115 @@ async function launchTui(command, options = {}) {
11378
11703
  term.options.fontSize = size;
11379
11704
  }, options.fontSize);
11380
11705
  }
11381
- return { ttydProcess, port, browser, page, theme: resolvedTheme };
11706
+ const method = options.method ?? "buffer";
11707
+ await configureDomRenderer(page, {
11708
+ active: isDomMethod(method),
11709
+ theme: resolvedTheme,
11710
+ fontSize: options.fontSize
11711
+ });
11712
+ return {
11713
+ ttydProcess,
11714
+ port,
11715
+ browser,
11716
+ page,
11717
+ theme: resolvedTheme,
11718
+ method,
11719
+ lastHealthCheck: Date.now(),
11720
+ reconnectCount: 0
11721
+ };
11382
11722
  } catch (err) {
11383
- ttydProcess.kill();
11723
+ try {
11724
+ ttydProcess.kill("SIGTERM");
11725
+ } catch {}
11384
11726
  throw err;
11385
11727
  }
11386
11728
  }
11729
+ async function getBufferState(page) {
11730
+ return page.evaluate(() => {
11731
+ const term = window.term ?? window.terminal;
11732
+ if (!term?.buffer?.active) {
11733
+ return {
11734
+ text: "",
11735
+ rows: [],
11736
+ row_count: 0,
11737
+ cols: null,
11738
+ total_rows: 0,
11739
+ buffer_length: null,
11740
+ cursor_row: -1,
11741
+ cursor_col: -1,
11742
+ font_size: null,
11743
+ theme: "dark"
11744
+ };
11745
+ }
11746
+ const buf = term.buffer.active;
11747
+ const rows = [];
11748
+ for (let i = 0;i < buf.length; i++) {
11749
+ const line = buf.getLine(i);
11750
+ if (line)
11751
+ rows.push(line.translateToString(true));
11752
+ }
11753
+ return {
11754
+ text: rows.join(`
11755
+ `).trimEnd(),
11756
+ rows,
11757
+ row_count: buf.length,
11758
+ cols: term.cols,
11759
+ total_rows: term.rows,
11760
+ buffer_length: buf.length,
11761
+ cursor_row: buf.cursorY,
11762
+ cursor_col: buf.cursorX,
11763
+ font_size: term.options?.fontSize ?? null,
11764
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
11765
+ };
11766
+ });
11767
+ }
11768
+ async function getDomState(page) {
11769
+ return readDomMirrorState(page);
11770
+ }
11771
+ async function getTerminalState(page, method = "buffer", timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
11772
+ return withTimeout("getTerminalState", async () => {
11773
+ const raw = method === "dom" ? await getDomState(page) : await getBufferState(page);
11774
+ const rows = raw.rows.map(normalizeRowText);
11775
+ const text = rows.join(`
11776
+ `).trimEnd();
11777
+ return {
11778
+ ...raw,
11779
+ method,
11780
+ rows,
11781
+ text,
11782
+ refs: buildRowRefs(rows, method, raw.total_rows, raw.row_count)
11783
+ };
11784
+ }, timeoutMs);
11785
+ }
11786
+ async function getTerminalText(page, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS, method = "buffer") {
11787
+ const state = await getTerminalState(page, method, timeoutMs);
11788
+ return state.text;
11789
+ }
11790
+ async function waitForTerminalText(page, text, timeoutMs = 30000, method = "buffer") {
11791
+ const start = Date.now();
11792
+ while (Date.now() - start < timeoutMs) {
11793
+ let healthy = false;
11794
+ try {
11795
+ await Promise.race([
11796
+ page.evaluate(() => {
11797
+ const term = window.term ?? window.terminal;
11798
+ return term?.buffer?.active ? true : false;
11799
+ }),
11800
+ new Promise((_, reject) => setTimeout(() => reject(new Error("probe timeout")), 2000))
11801
+ ]);
11802
+ healthy = true;
11803
+ } catch {}
11804
+ if (!healthy)
11805
+ return { found: false, elapsed_ms: Date.now() - start, stuck: true };
11806
+ const content = await getTerminalText(page, DEFAULT_TOOL_TIMEOUT_MS, method);
11807
+ if (content.includes(text))
11808
+ return { found: true, elapsed_ms: Date.now() - start, stuck: false };
11809
+ await new Promise((r) => setTimeout(r, 250));
11810
+ }
11811
+ return { found: false, elapsed_ms: timeoutMs, stuck: false };
11812
+ }
11387
11813
  async function closeTui(session) {
11814
+ await destroyDomRenderer(session.page);
11388
11815
  try {
11389
11816
  await session.page.close();
11390
11817
  } catch {}
@@ -11394,8 +11821,11 @@ async function closeTui(session) {
11394
11821
  try {
11395
11822
  session.ttydProcess.kill("SIGTERM");
11396
11823
  } catch {}
11824
+ try {
11825
+ session.ttydProcess.kill("SIGKILL");
11826
+ } catch {}
11397
11827
  }
11398
- var DEFAULT_TTYD_PORT_START = 7780, nextPort, THEMES;
11828
+ var DEFAULT_TTYD_PORT_START = 7780, nextPort, DEFAULT_TOOL_TIMEOUT_MS = 15000, HEALTH_CHECK_TIMEOUT_MS = 3000, THEMES;
11399
11829
  var init_tui = __esm(() => {
11400
11830
  init_types2();
11401
11831
  init_playwright();
@@ -11721,19 +12151,13 @@ Object.defineProperty(navigator, 'plugins', {
11721
12151
  { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
11722
12152
  { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
11723
12153
  ];
11724
- // Mimic PluginArray interface
11725
- const pluginArray = Object.create(PluginArray.prototype);
12154
+ // Mimic PluginArray interface \u2014 guard against removed prototypes
12155
+ const pluginArray = {};
11726
12156
  plugins.forEach((p, i) => {
11727
- const plugin = Object.create(Plugin.prototype);
11728
- Object.defineProperties(plugin, {
11729
- name: { value: p.name, enumerable: true },
11730
- filename: { value: p.filename, enumerable: true },
11731
- description: { value: p.description, enumerable: true },
11732
- length: { value: p.length, enumerable: true },
11733
- });
12157
+ const plugin = { ...p, item: () => null };
11734
12158
  pluginArray[i] = plugin;
11735
12159
  });
11736
- Object.defineProperty(pluginArray, 'length', { value: plugins.length });
12160
+ pluginArray.length = plugins.length;
11737
12161
  pluginArray.item = (i) => pluginArray[i] || null;
11738
12162
  pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
11739
12163
  pluginArray.refresh = () => {};
@@ -12678,6 +13102,7 @@ function watchPage(page, opts) {
12678
13102
  const changes = [];
12679
13103
  const intervalMs = opts?.intervalMs ?? 500;
12680
13104
  const maxChanges = opts?.maxChanges ?? 50;
13105
+ const sessionId = opts?.sessionId;
12681
13106
  const interval = setInterval(async () => {
12682
13107
  if (changes.length >= maxChanges)
12683
13108
  return;
@@ -12691,7 +13116,7 @@ function watchPage(page, opts) {
12691
13116
  }
12692
13117
  } catch {}
12693
13118
  }, intervalMs);
12694
- activeWatches.set(id, { interval, changes });
13119
+ activeWatches.set(id, { interval, changes, sessionId });
12695
13120
  return {
12696
13121
  id,
12697
13122
  stop: () => {
@@ -12710,10 +13135,12 @@ function stopWatch(watchId) {
12710
13135
  activeWatches.delete(watchId);
12711
13136
  }
12712
13137
  }
12713
- function stopAllWatchesForSession(_sessionId) {
12714
- for (const [id, w] of activeWatches) {
12715
- clearInterval(w.interval);
12716
- activeWatches.delete(id);
13138
+ function stopAllWatchesForSession(sessionId) {
13139
+ for (const [id, w] of [...activeWatches]) {
13140
+ if (!sessionId || w.sessionId === sessionId) {
13141
+ clearInterval(w.interval);
13142
+ activeWatches.delete(id);
13143
+ }
12717
13144
  }
12718
13145
  }
12719
13146
  async function clickRef(page, sessionId, ref, opts) {
@@ -12794,6 +13221,7 @@ var init_actions = __esm(() => {
12794
13221
  // src/lib/session.ts
12795
13222
  var exports_session = {};
12796
13223
  __export(exports_session, {
13224
+ setSessionTui: () => setSessionTui,
12797
13225
  setSessionPage: () => setSessionPage,
12798
13226
  renameSession: () => renameSession2,
12799
13227
  listSessions: () => listSessions2,
@@ -12801,8 +13229,10 @@ __export(exports_session, {
12801
13229
  isAutoGallery: () => isAutoGallery,
12802
13230
  hasActiveHandle: () => hasActiveHandle,
12803
13231
  getTokenBudget: () => getTokenBudget,
13232
+ getSessionTuiSession: () => getSessionTuiSession,
12804
13233
  getSessionPage: () => getSessionPage,
12805
13234
  getSessionEngine: () => getSessionEngine,
13235
+ getSessionCommand: () => getSessionCommand,
12806
13236
  getSessionByName: () => getSessionByName2,
12807
13237
  getSessionBunView: () => getSessionBunView,
12808
13238
  getSessionBrowser: () => getSessionBrowser,
@@ -12848,7 +13278,7 @@ async function createSession2(opts = {}) {
12848
13278
  try {
12849
13279
  cleanups2.push(setupDialogHandler(page2, session2.id));
12850
13280
  } catch {}
12851
- 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 });
13281
+ 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 ?? "" });
12852
13282
  return { session: session2, page: page2 };
12853
13283
  }
12854
13284
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
@@ -12862,13 +13292,29 @@ async function createSession2(opts = {}) {
12862
13292
  browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
12863
13293
  page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
12864
13294
  } else {
12865
- bunView = new BunWebViewSession({
13295
+ const testView = new BunWebViewSession({
12866
13296
  width: opts.viewport?.width ?? 1280,
12867
13297
  height: opts.viewport?.height ?? 720,
12868
13298
  profile: opts.name ?? undefined
12869
13299
  });
12870
- if (opts.stealth) {}
12871
- page = createBunProxy(bunView);
13300
+ let bunWorks = true;
13301
+ try {
13302
+ await testView.goto("data:text/html,<html></html>");
13303
+ } catch {
13304
+ bunWorks = false;
13305
+ try {
13306
+ await testView.close();
13307
+ } catch {}
13308
+ }
13309
+ if (!bunWorks) {
13310
+ console.warn("[browser] Bun.WebView exists but Chrome not available \u2014 falling back to playwright");
13311
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
13312
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
13313
+ } else {
13314
+ bunView = testView;
13315
+ if (opts.stealth) {}
13316
+ page = createBunProxy(bunView);
13317
+ }
12872
13318
  }
12873
13319
  } else if (resolvedEngine === "lightpanda") {
12874
13320
  browser = await connectLightpanda();
@@ -12880,7 +13326,8 @@ async function createSession2(opts = {}) {
12880
13326
  headless: opts.headless ?? true,
12881
13327
  viewport: opts.viewport,
12882
13328
  theme: opts.tuiTheme ?? "system",
12883
- fontSize: opts.tuiFontSize
13329
+ fontSize: opts.tuiFontSize,
13330
+ method: opts.tuiMethod ?? "buffer"
12884
13331
  });
12885
13332
  browser = tuiSess.browser;
12886
13333
  page = tuiSess.page;
@@ -12906,7 +13353,7 @@ async function createSession2(opts = {}) {
12906
13353
  try {
12907
13354
  cleanups2.push(setupDialogHandler(page, session2.id));
12908
13355
  } catch {}
12909
- 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 });
13356
+ 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" });
12910
13357
  return { session: session2, page };
12911
13358
  } else {
12912
13359
  browser = await pool.acquire(opts.headless ?? true);
@@ -12978,7 +13425,7 @@ async function createSession2(opts = {}) {
12978
13425
  } catch {}
12979
13426
  }
12980
13427
  }
12981
- 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 });
13428
+ 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 ?? "" });
12982
13429
  if (opts.startUrl) {
12983
13430
  try {
12984
13431
  if (bunView) {
@@ -13030,6 +13477,23 @@ function getSessionEngine(sessionId) {
13030
13477
  function hasActiveHandle(sessionId) {
13031
13478
  return handles.has(sessionId);
13032
13479
  }
13480
+ function getSessionTuiSession(sessionId) {
13481
+ return handles.get(sessionId)?.tuiSession ?? null;
13482
+ }
13483
+ function setSessionTui(sessionId, tuiSess) {
13484
+ const handle = handles.get(sessionId);
13485
+ if (!handle)
13486
+ throw new SessionNotFoundError(sessionId);
13487
+ handle.tuiSession = tuiSess;
13488
+ handle.page = tuiSess.page;
13489
+ if (tuiSess.browser !== handle.browser) {
13490
+ handle.browser = tuiSess.browser;
13491
+ }
13492
+ handle.lastActivity = Date.now();
13493
+ }
13494
+ function getSessionCommand(sessionId) {
13495
+ return handles.get(sessionId)?.startUrl ?? "bash";
13496
+ }
13033
13497
  function setSessionPage(sessionId, page) {
13034
13498
  const handle = handles.get(sessionId);
13035
13499
  if (!handle)
@@ -13038,38 +13502,43 @@ function setSessionPage(sessionId, page) {
13038
13502
  }
13039
13503
  async function closeSession2(sessionId) {
13040
13504
  const handle = handles.get(sessionId);
13041
- if (handle) {
13042
- for (const cleanup of handle.cleanups) {
13043
- try {
13044
- cleanup();
13045
- } catch {}
13046
- }
13047
- if (handle.bunView) {
13048
- try {
13049
- await handle.bunView.close();
13050
- } catch {}
13051
- } else if (handle.tuiSession) {} else {
13052
- try {
13053
- await handle.page.context().close();
13054
- } catch {}
13055
- if (handle.browser)
13056
- pool.release(handle.browser);
13505
+ try {
13506
+ if (handle) {
13507
+ for (const cleanup of handle.cleanups) {
13508
+ try {
13509
+ cleanup();
13510
+ } catch {}
13511
+ }
13512
+ if (handle.bunView) {
13513
+ try {
13514
+ await handle.bunView.close();
13515
+ } catch {}
13516
+ } else if (handle.tuiSession) {} else {
13517
+ try {
13518
+ await handle.page.context().close();
13519
+ } catch {}
13520
+ try {
13521
+ if (handle.browser)
13522
+ pool.release(handle.browser);
13523
+ } catch {}
13524
+ }
13057
13525
  }
13526
+ try {
13527
+ const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
13528
+ clearLastSnapshot2(sessionId);
13529
+ clearSessionRefs2(sessionId);
13530
+ } catch {}
13531
+ try {
13532
+ const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
13533
+ stopAllWatchesForSession2(sessionId);
13534
+ } catch {}
13535
+ try {
13536
+ const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
13537
+ clearDialogs2(sessionId);
13538
+ } catch {}
13539
+ } finally {
13058
13540
  handles.delete(sessionId);
13059
13541
  }
13060
- try {
13061
- const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
13062
- clearLastSnapshot2(sessionId);
13063
- clearSessionRefs2(sessionId);
13064
- } catch {}
13065
- try {
13066
- const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
13067
- stopAllWatchesForSession2(sessionId);
13068
- } catch {}
13069
- try {
13070
- const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
13071
- clearDialogs2(sessionId);
13072
- } catch {}
13073
13542
  return closeSession(sessionId);
13074
13543
  }
13075
13544
  function getSession2(sessionId) {
@@ -13170,14 +13639,16 @@ var init_session = __esm(() => {
13170
13639
  ttlInterval.unref();
13171
13640
  DB_PRUNE_INTERVAL_MS = 30 * 60000;
13172
13641
  dbPruneInterval = setInterval(() => {
13173
- try {
13174
- const { getDatabase: getDatabase2 } = (init_schema(), __toCommonJS(exports_schema));
13175
- const db = getDatabase2();
13176
- const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
13177
- db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13178
- db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13179
- db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13180
- } catch {}
13642
+ (async () => {
13643
+ try {
13644
+ const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
13645
+ const db = getDatabase2();
13646
+ const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
13647
+ db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13648
+ db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13649
+ db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
13650
+ } catch {}
13651
+ })();
13181
13652
  }, DB_PRUNE_INTERVAL_MS);
13182
13653
  if (dbPruneInterval.unref)
13183
13654
  dbPruneInterval.unref();
@@ -19774,6 +20245,12 @@ __export(exports_gallery, {
19774
20245
  createEntry: () => createEntry
19775
20246
  });
19776
20247
  import { randomUUID as randomUUID4 } from "crypto";
20248
+ function validateDataPath(filePath) {
20249
+ if (filePath.includes("..")) {
20250
+ throw new Error(`File path must not contain '..': ${filePath}`);
20251
+ }
20252
+ return filePath;
20253
+ }
19777
20254
  function deserialize(row) {
19778
20255
  return {
19779
20256
  id: row.id,
@@ -19798,6 +20275,9 @@ function deserialize(row) {
19798
20275
  function createEntry(data) {
19799
20276
  const db = getDatabase();
19800
20277
  const id = randomUUID4();
20278
+ validateDataPath(data.path);
20279
+ if (data.thumbnail_path)
20280
+ validateDataPath(data.thumbnail_path);
19801
20281
  db.prepare(`
19802
20282
  INSERT INTO gallery_entries
19803
20283
  (id, session_id, project_id, url, title, path, thumbnail_path, format,
@@ -23527,11 +24007,14 @@ import { join as join15 } from "path";
23527
24007
  import { homedir as homedir9 } from "os";
23528
24008
  async function getCredentials(service) {
23529
24009
  try {
23530
- const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
23531
- const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
23532
- const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
23533
- if (email?.value && password?.value) {
23534
- return { email: email.value, password: password.value };
24010
+ const secretsVaultPath = process.env["BROWSER_SECRETS_VAULT_PATH"];
24011
+ if (secretsVaultPath) {
24012
+ const { getSecret } = await import(secretsVaultPath);
24013
+ const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
24014
+ const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
24015
+ if (email?.value && password?.value) {
24016
+ return { email: email.value, password: password.value };
24017
+ }
23535
24018
  }
23536
24019
  } catch {}
23537
24020
  const secretsPath = join15(homedir9(), ".secrets");
@@ -24600,11 +25083,21 @@ async function execBrowser(cfg, step, page, vars) {
24600
25083
  break;
24601
25084
  }
24602
25085
  }
25086
+ function isValidArg(arg) {
25087
+ return /^[a-zA-Z0-9._\-/@:]+$/.test(arg);
25088
+ }
24603
25089
  async function execConnector(cfg, step, vars) {
24604
25090
  const connector = cfg.connector;
24605
25091
  if (!connector)
24606
25092
  throw new Error("Connector step missing 'connector' in config");
24607
- const args = cfg.args ?? [];
25093
+ if (!ALLOWED_CONNECTORS.has(connector)) {
25094
+ throw new Error(`Unknown connector '${connector}'. Allowed: ${[...ALLOWED_CONNECTORS].join(", ")}`);
25095
+ }
25096
+ const args = (cfg.args ?? []).filter((a) => typeof a === "string").filter((a) => {
25097
+ if (!isValidArg(a))
25098
+ throw new Error(`Connector arg '${a}' contains disallowed characters`);
25099
+ return true;
25100
+ });
24608
25101
  const proc = Bun.spawn([`connect-${connector}`, ...args], {
24609
25102
  stdout: "pipe",
24610
25103
  stderr: "pipe",
@@ -24682,9 +25175,11 @@ async function aiSelfHeal(page, description, step) {
24682
25175
  function decodeHtmlEntities(str) {
24683
25176
  return str.replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&#x27;/g, "'");
24684
25177
  }
25178
+ var ALLOWED_CONNECTORS;
24685
25179
  var init_script_engine = __esm(() => {
24686
25180
  init_scripts();
24687
25181
  init_ai_inference();
25182
+ ALLOWED_CONNECTORS = new Set(["github", "linear", "slack", "jira", "notion", "gitlab"]);
24688
25183
  });
24689
25184
 
24690
25185
  // src/lib/api-detector.ts
@@ -75043,7 +75538,7 @@ ENGINES:
75043
75538
  - "cdp": Chrome DevTools Protocol \u2014 network monitoring, perf profiling, script injection
75044
75539
  - "lightpanda": fast headless for static pages
75045
75540
  - "bun": native Bun.WebView \u2014 fastest for screenshots and scraping
75046
- - "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.
75541
+ - "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.
75047
75542
 
75048
75543
  TIPS:
75049
75544
  - If agent_id is set and already has an active session, returns the existing one (use force_new to override)
@@ -75065,8 +75560,9 @@ TIPS:
75065
75560
  tags: exports_external2.array(exports_external2.string()).optional(),
75066
75561
  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"),
75067
75562
  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."),
75068
- 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.")
75069
- }, 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 }) => {
75563
+ 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."),
75564
+ 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.")
75565
+ }, 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 }) => {
75070
75566
  try {
75071
75567
  if (agent_id && !force_new) {
75072
75568
  const existing = getActiveSessionForAgent2(agent_id);
@@ -75086,7 +75582,8 @@ TIPS:
75086
75582
  storageState: storage_state,
75087
75583
  cdpUrl: cdp_url,
75088
75584
  tuiTheme: tui_theme,
75089
- tuiFontSize: tui_font_size
75585
+ tuiFontSize: tui_font_size,
75586
+ tuiMethod: tui_method
75090
75587
  });
75091
75588
  if (tags?.length) {
75092
75589
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -75541,6 +76038,13 @@ function register2(server) {
75541
76038
  });
75542
76039
  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 }) => {
75543
76040
  try {
76041
+ if (file_path.includes("..")) {
76042
+ return err(new Error("File path must not contain '..'"));
76043
+ }
76044
+ const { existsSync: existsSync5 } = await import("fs");
76045
+ if (!existsSync5(file_path)) {
76046
+ return err(new Error(`File not found: ${file_path}`));
76047
+ }
75544
76048
  const sid = resolveSessionId(session_id);
75545
76049
  const page = getSessionPage(sid);
75546
76050
  await uploadFile(page, selector, file_path);
@@ -76100,14 +76604,14 @@ function register3(server) {
76100
76604
  } else if (/^title\s+contains\s+/i.test(trimmed)) {
76101
76605
  const needle = trimmed.replace(/^title\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
76102
76606
  result = (await getTitle(page)).toLowerCase().includes(needle.toLowerCase());
76103
- } else if (/^text:["'](.+)["']/i.test(trimmed)) {
76104
- const text = trimmed.match(/^text:["'](.+)["']/i)?.[1] ?? "";
76607
+ } else if (/^text:["']([^"']+)["']/i.test(trimmed)) {
76608
+ const text = trimmed.match(/^text:["']([^"']+)["']/i)?.[1] ?? "";
76105
76609
  result = await page.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)}) ?? false`);
76106
76610
  } else if (/^element:["'](.+)["']/i.test(trimmed)) {
76107
76611
  const sel = trimmed.match(/^element:["'](.+)["']/i)?.[1] ?? "";
76108
76612
  result = await page.evaluate(`!!document.querySelector(${JSON.stringify(sel)})`);
76109
- } else if (/^count:["'](.+)["']\s*([><=!]+)\s*(\d+)/i.test(trimmed)) {
76110
- const [, sel, op, n] = trimmed.match(/^count:["'](.+)["']\s*([><=!]+)\s*(\d+)/i);
76613
+ } else if (/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i.test(trimmed)) {
76614
+ const [, sel, op, n] = trimmed.match(/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i);
76111
76615
  const count = await page.evaluate(`document.querySelectorAll(${JSON.stringify(sel)}).length`);
76112
76616
  const num = parseInt(n);
76113
76617
  result = op === ">" ? count > num : op === ">=" ? count >= num : op === "<" ? count < num : op === "<=" ? count <= num : count === num;
@@ -76876,12 +77380,12 @@ function register6(server) {
76876
77380
  });
76877
77381
  }
76878
77382
 
76879
- // src/mcp/meta.ts
76880
- function register7(server) {
77383
+ // src/mcp/agents.ts
77384
+ function registerAgentsAndProjects(server) {
76881
77385
  server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
76882
77386
  name: exports_external2.string(),
76883
77387
  description: exports_external2.string().optional(),
76884
- session_id: exports_external2.string().optional().optional(),
77388
+ session_id: exports_external2.string().optional(),
76885
77389
  project_id: exports_external2.string().optional(),
76886
77390
  working_dir: exports_external2.string().optional()
76887
77391
  }, async ({ name, description, session_id, project_id, working_dir }) => {
@@ -76931,9 +77435,13 @@ function register7(server) {
76931
77435
  return err(e);
76932
77436
  }
76933
77437
  });
77438
+ }
77439
+
77440
+ // src/mcp/gallery.ts
77441
+ function registerGalleryAndDownloads(server) {
76934
77442
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
76935
77443
  project_id: exports_external2.string().optional(),
76936
- session_id: exports_external2.string().optional().optional(),
77444
+ session_id: exports_external2.string().optional(),
76937
77445
  tag: exports_external2.string().optional(),
76938
77446
  is_favorite: exports_external2.boolean().optional(),
76939
77447
  date_from: exports_external2.string().optional(),
@@ -77021,14 +77529,14 @@ function register7(server) {
77021
77529
  return err(e);
77022
77530
  }
77023
77531
  });
77024
- server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional().optional() }, async ({ session_id }) => {
77532
+ server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
77025
77533
  try {
77026
77534
  return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
77027
77535
  } catch (e) {
77028
77536
  return err(e);
77029
77537
  }
77030
77538
  });
77031
- 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 }) => {
77539
+ 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 }) => {
77032
77540
  try {
77033
77541
  const file = getDownload(id, session_id);
77034
77542
  if (!file)
@@ -77039,10 +77547,9 @@ function register7(server) {
77039
77547
  return err(e);
77040
77548
  }
77041
77549
  });
77042
- 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 }) => {
77550
+ 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 }) => {
77043
77551
  try {
77044
- const deleted = deleteDownload(id, session_id);
77045
- return json({ deleted });
77552
+ return json({ deleted: deleteDownload(id, session_id) });
77046
77553
  } catch (e) {
77047
77554
  return err(e);
77048
77555
  }
@@ -77054,7 +77561,7 @@ function register7(server) {
77054
77561
  return err(e);
77055
77562
  }
77056
77563
  });
77057
- 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 }) => {
77564
+ 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 }) => {
77058
77565
  try {
77059
77566
  const finalPath = exportToPath(id, target_path, session_id);
77060
77567
  return json({ path: finalPath });
@@ -77079,6 +77586,10 @@ function register7(server) {
77079
77586
  return err(e);
77080
77587
  }
77081
77588
  });
77589
+ }
77590
+
77591
+ // src/mcp/integration.ts
77592
+ function registerIntegrationAndMeta(server) {
77082
77593
  const activeWatchHandles2 = new Map;
77083
77594
  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 }) => {
77084
77595
  try {
@@ -77108,214 +77619,7 @@ function register7(server) {
77108
77619
  return err(e);
77109
77620
  }
77110
77621
  });
77111
- server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
77112
- try {
77113
- const groups = {
77114
- Navigation: [
77115
- { tool: "browser_navigate", description: "Navigate to a URL" },
77116
- { tool: "browser_back", description: "Navigate back in history" },
77117
- { tool: "browser_forward", description: "Navigate forward in history" },
77118
- { tool: "browser_reload", description: "Reload the current page" },
77119
- { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
77120
- { tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
77121
- ],
77122
- Interaction: [
77123
- { tool: "browser_click", description: "Click element by ref or selector" },
77124
- { tool: "browser_click_text", description: "Click element by visible text" },
77125
- { tool: "browser_type", description: "Type text into an element" },
77126
- { tool: "browser_hover", description: "Hover over an element" },
77127
- { tool: "browser_scroll", description: "Scroll the page" },
77128
- { tool: "browser_select", description: "Select a dropdown option" },
77129
- { tool: "browser_toggle", description: "Check/uncheck a checkbox" },
77130
- { tool: "browser_upload", description: "Upload a file to an input" },
77131
- { tool: "browser_press_key", description: "Press a keyboard key" },
77132
- { tool: "browser_wait", description: "Wait for a selector to appear" },
77133
- { tool: "browser_wait_for_text", description: "Wait for text to appear" },
77134
- { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
77135
- { tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
77136
- { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
77137
- ],
77138
- Extraction: [
77139
- { tool: "browser_get_text", description: "Get text content from page/selector" },
77140
- { tool: "browser_get_html", description: "Get HTML content from page/selector" },
77141
- { tool: "browser_get_links", description: "Get all links on the page" },
77142
- { tool: "browser_get_page_info", description: "Full page summary in one call" },
77143
- { tool: "browser_extract", description: "Extract content in various formats" },
77144
- { tool: "browser_find", description: "Find elements by selector" },
77145
- { tool: "browser_element_exists", description: "Check if a selector exists" },
77146
- { tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
77147
- { tool: "browser_evaluate", description: "Execute JavaScript in page context" }
77148
- ],
77149
- Capture: [
77150
- { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
77151
- { tool: "browser_pdf", description: "Generate a PDF of the page" },
77152
- { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
77153
- { tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" },
77154
- { tool: "browser_diff", description: "Visual diff between two URLs \u2014 highlights changes in red" }
77155
- ],
77156
- Storage: [
77157
- { tool: "browser_cookies_get", description: "Get cookies" },
77158
- { tool: "browser_cookies_set", description: "Set a cookie" },
77159
- { tool: "browser_cookies_clear", description: "Clear cookies" },
77160
- { tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
77161
- { tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
77162
- { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
77163
- { tool: "browser_profile_load", description: "Load and apply a saved profile" },
77164
- { tool: "browser_profile_list", description: "List saved profiles" },
77165
- { tool: "browser_profile_delete", description: "Delete a saved profile" },
77166
- { tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
77167
- { tool: "browser_session_list_states", description: "List saved storage states" },
77168
- { tool: "browser_session_delete_state", description: "Delete a saved storage state" }
77169
- ],
77170
- Network: [
77171
- { tool: "browser_network_log", description: "Get captured network requests" },
77172
- { tool: "browser_network_intercept", description: "Add a network interception rule" },
77173
- { tool: "browser_har_start", description: "Start HAR capture" },
77174
- { tool: "browser_har_stop", description: "Stop HAR capture and get data" },
77175
- { tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
77176
- { tool: "browser_intercept_clear", description: "Remove all response intercepts" }
77177
- ],
77178
- Performance: [
77179
- { tool: "browser_performance", description: "Get performance metrics" },
77180
- { tool: "browser_performance_budget", description: "Check perf against budget thresholds (LCP, FCP, CLS, TTFB)" }
77181
- ],
77182
- Console: [
77183
- { tool: "browser_console_log", description: "Get console messages" },
77184
- { tool: "browser_has_errors", description: "Check for console errors" },
77185
- { tool: "browser_clear_errors", description: "Clear console error log" },
77186
- { tool: "browser_get_dialogs", description: "Get pending dialogs" }
77187
- ],
77188
- Recording: [
77189
- { tool: "browser_record_start", description: "Start recording actions" },
77190
- { tool: "browser_record_step", description: "Add a step to recording" },
77191
- { tool: "browser_record_stop", description: "Stop and save recording" },
77192
- { tool: "browser_record_replay", description: "Replay a recorded sequence" },
77193
- { tool: "browser_record_export", description: "Export recording as Playwright test, Puppeteer script, or JSON" },
77194
- { tool: "browser_recordings_list", description: "List all recordings" }
77195
- ],
77196
- Auth: [
77197
- { tool: "browser_auth_record", description: "Start recording a login flow" },
77198
- { tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
77199
- { tool: "browser_auth_replay", description: "Replay a saved auth flow" },
77200
- { tool: "browser_auth_list", description: "List all saved auth flows" },
77201
- { tool: "browser_auth_delete", description: "Delete a saved auth flow" }
77202
- ],
77203
- Workflows: [
77204
- { tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
77205
- { tool: "browser_workflow_list", description: "List all saved workflows" },
77206
- { tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
77207
- { tool: "browser_workflow_delete", description: "Delete a saved workflow" }
77208
- ],
77209
- Data: [
77210
- { tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
77211
- { tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
77212
- { tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
77213
- { tool: "browser_dataset_list", description: "List all saved datasets" },
77214
- { tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
77215
- { tool: "browser_dataset_delete", description: "Delete a saved dataset" }
77216
- ],
77217
- Crawl: [
77218
- { tool: "browser_crawl", description: "Crawl a URL recursively" }
77219
- ],
77220
- Agent: [
77221
- { tool: "register_agent", description: "Register an agent session" },
77222
- { tool: "heartbeat", description: "Update agent last_seen_at" },
77223
- { tool: "list_agents", description: "List registered agents" },
77224
- { tool: "set_focus", description: "Set active project context" }
77225
- ],
77226
- Project: [
77227
- { tool: "browser_project_create", description: "Create or ensure a project" },
77228
- { tool: "browser_project_list", description: "List all projects" }
77229
- ],
77230
- Gallery: [
77231
- { tool: "browser_gallery_list", description: "List screenshot gallery entries" },
77232
- { tool: "browser_gallery_get", description: "Get a gallery entry by id" },
77233
- { tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
77234
- { tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
77235
- { tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
77236
- { tool: "browser_gallery_delete", description: "Delete a gallery entry" },
77237
- { tool: "browser_gallery_search", description: "Search gallery entries" },
77238
- { tool: "browser_gallery_stats", description: "Get gallery statistics" },
77239
- { tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
77240
- ],
77241
- Downloads: [
77242
- { tool: "browser_downloads_list", description: "List downloaded files" },
77243
- { tool: "browser_downloads_get", description: "Get a download by id" },
77244
- { tool: "browser_downloads_delete", description: "Delete a download" },
77245
- { tool: "browser_downloads_clean", description: "Clean old downloads" },
77246
- { tool: "browser_downloads_export", description: "Copy download to a path" },
77247
- { tool: "browser_persist_file", description: "Persist file permanently" }
77248
- ],
77249
- Session: [
77250
- { tool: "browser_session_create", description: "Create a new browser session" },
77251
- { tool: "browser_session_list", description: "List all sessions" },
77252
- { tool: "browser_session_close", description: "Close a session" },
77253
- { tool: "browser_session_get_by_name", description: "Get session by name" },
77254
- { tool: "browser_session_rename", description: "Rename a session" },
77255
- { tool: "browser_session_lock", description: "Lock a session for an agent" },
77256
- { tool: "browser_session_unlock", description: "Unlock a session" },
77257
- { tool: "browser_session_transfer", description: "Transfer session to another agent" },
77258
- { tool: "browser_session_tag", description: "Add a tag to a session" },
77259
- { tool: "browser_session_untag", description: "Remove a tag from a session" },
77260
- { tool: "browser_session_stats", description: "Get session stats and token usage" },
77261
- { tool: "browser_session_timeline", description: "Get chronological action log" },
77262
- { tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
77263
- { tool: "browser_tab_new", description: "Open a new tab" },
77264
- { tool: "browser_tab_list", description: "List all open tabs" },
77265
- { tool: "browser_tab_switch", description: "Switch to a tab by index" },
77266
- { tool: "browser_tab_close", description: "Close a tab by index" }
77267
- ],
77268
- TUI: [
77269
- { tool: "browser_tui_send_keys", description: "Send keystrokes (ctrl+c, arrow_up, tab, enter, etc.)" },
77270
- { tool: "browser_tui_send_text", description: "Type text + optional Enter (most common TUI interaction)" },
77271
- { tool: "browser_tui_resize", description: "Resize terminal cols/rows mid-session" },
77272
- { tool: "browser_tui_get_text", description: "Get terminal text buffer (full or row range)" },
77273
- { tool: "browser_tui_wait_for_text", description: "Wait for text to appear in terminal output" },
77274
- { tool: "browser_tui_get_cursor", description: "Get cursor position (row, col)" },
77275
- { tool: "browser_tui_assert", description: "Assert terminal conditions (text contains, row N contains, cursor at)" },
77276
- { tool: "browser_tui_snapshot", description: "Structured terminal snapshot (rows array, cursor, dimensions)" },
77277
- { tool: "browser_tui_record_start", description: "Start recording terminal as asciicast" },
77278
- { tool: "browser_tui_record_stop", description: "Stop recording, return asciicast v2 JSON" }
77279
- ],
77280
- Meta: [
77281
- { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
77282
- { tool: "browser_version", description: "Show running binary version and tool count" },
77283
- { tool: "browser_help", description: "Show this help (all tools)" },
77284
- { tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
77285
- { tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
77286
- { tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
77287
- { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
77288
- { tool: "browser_watch_start", description: "Watch page for DOM changes" },
77289
- { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
77290
- { tool: "browser_watch_stop", description: "Stop DOM watcher" },
77291
- { tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
77292
- ]
77293
- };
77294
- const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
77295
- return json({ groups, total_tools: totalTools });
77296
- } catch (e) {
77297
- return err(e);
77298
- }
77299
- });
77300
- server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
77301
- try {
77302
- const { getDataDir: getDataDir7 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
77303
- const toolCount = Object.keys(server._registeredTools ?? {}).length;
77304
- const { readFileSync: readFileSync7 } = await import("fs");
77305
- const { join: join20 } = await import("path");
77306
- const _pkg = JSON.parse(readFileSync7(join20(import.meta.dir, "../../package.json"), "utf8"));
77307
- return json({
77308
- version: _pkg.version,
77309
- mcp_tools_count: toolCount,
77310
- bun_version: Bun.version,
77311
- data_dir: getDataDir7(),
77312
- node_env: process.env["NODE_ENV"] ?? "production"
77313
- });
77314
- } catch (e) {
77315
- return err(e);
77316
- }
77317
- });
77318
- 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 }) => {
77622
+ 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 }) => {
77319
77623
  try {
77320
77624
  const sid = resolveSessionId(session_id);
77321
77625
  const page = getSessionPage(sid);
@@ -77332,7 +77636,7 @@ function register7(server) {
77332
77636
  return err(e);
77333
77637
  }
77334
77638
  });
77335
- 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 }) => {
77639
+ 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 }) => {
77336
77640
  try {
77337
77641
  const sid = resolveSessionId(session_id);
77338
77642
  const page = getSessionPage(sid);
@@ -77344,7 +77648,7 @@ function register7(server) {
77344
77648
  return err(e);
77345
77649
  }
77346
77650
  });
77347
- 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 }) => {
77651
+ 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 }) => {
77348
77652
  try {
77349
77653
  const { recallPage: recallPage2 } = await Promise.resolve().then(() => (init_page_memory(), exports_page_memory));
77350
77654
  const memory = await recallPage2(url, max_age_hours);
@@ -77365,7 +77669,7 @@ function register7(server) {
77365
77669
  return err(e);
77366
77670
  }
77367
77671
  });
77368
- 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 }) => {
77672
+ server.tool("browser_check_navigation", "Check if another agent is already scraping this URL.", { url: exports_external2.string() }, async ({ url }) => {
77369
77673
  try {
77370
77674
  const { checkDuplicate: checkDuplicate3 } = await Promise.resolve().then(() => (init_coordination(), exports_coordination));
77371
77675
  return json(await checkDuplicate3(url));
@@ -77399,7 +77703,7 @@ function register7(server) {
77399
77703
  return err(e);
77400
77704
  }
77401
77705
  });
77402
- 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 }) => {
77706
+ 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 }) => {
77403
77707
  try {
77404
77708
  const sid = resolveSessionId(session_id);
77405
77709
  const page = getSessionPage(sid);
@@ -77417,7 +77721,7 @@ function register7(server) {
77417
77721
  return err(e);
77418
77722
  }
77419
77723
  });
77420
- server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot. Eliminates 80% of round trips for multi-step flows.", {
77724
+ server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot.", {
77421
77725
  session_id: exports_external2.string().optional(),
77422
77726
  actions: exports_external2.array(exports_external2.object({
77423
77727
  tool: exports_external2.string(),
@@ -77456,8 +77760,8 @@ function register7(server) {
77456
77760
  break;
77457
77761
  case "fill_form":
77458
77762
  if (args.fields) {
77459
- const { fillForm: fillForm3 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
77460
- const r = await fillForm3(page, args.fields);
77763
+ const { fillForm: fillForm2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
77764
+ const r = await fillForm2(page, args.fields);
77461
77765
  results.push({ tool: action.tool, success: true, result: r });
77462
77766
  }
77463
77767
  break;
@@ -77507,13 +77811,13 @@ function register7(server) {
77507
77811
  return err(e);
77508
77812
  }
77509
77813
  });
77510
- server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
77814
+ server.tool("browser_parallel", "Execute actions across multiple sessions in parallel.", {
77511
77815
  actions: exports_external2.array(exports_external2.object({
77512
- session_id: exports_external2.string().describe("Target session ID"),
77513
- tool: exports_external2.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
77816
+ session_id: exports_external2.string(),
77817
+ tool: exports_external2.string(),
77514
77818
  args: exports_external2.record(exports_external2.unknown()).optional().default({})
77515
77819
  })),
77516
- timeout: exports_external2.number().optional().default(30000).describe("Timeout per action in ms")
77820
+ timeout: exports_external2.number().optional().default(30000)
77517
77821
  }, async ({ actions, timeout }) => {
77518
77822
  try {
77519
77823
  const t0 = Date.now();
@@ -77575,22 +77879,21 @@ function register7(server) {
77575
77879
  }
77576
77880
  });
77577
77881
  const results = await Promise.all(promises);
77578
- const duration_ms = Date.now() - t0;
77579
77882
  const succeeded = results.filter((r) => r.success).length;
77580
77883
  const failed = results.filter((r) => !r.success).length;
77581
- return json({ results, duration_ms, succeeded, failed, total: actions.length });
77884
+ return json({ results, duration_ms: Date.now() - t0, succeeded, failed, total: actions.length });
77582
77885
  } catch (e) {
77583
77886
  return err(e);
77584
77887
  }
77585
77888
  });
77586
77889
  server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
77587
77890
  try {
77588
- return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
77891
+ return json({ message: "Session pool not yet implemented in this version.", ready: 0, total: 0 });
77589
77892
  } catch (e) {
77590
77893
  return err(e);
77591
77894
  }
77592
77895
  });
77593
- 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 }) => {
77896
+ 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 }) => {
77594
77897
  try {
77595
77898
  const { createCronJob: createCronJob2 } = await Promise.resolve().then(() => (init_cron_manager(), exports_cron_manager));
77596
77899
  return json(createCronJob2(schedule, { url, skill, extract: extract2 }, name));
@@ -77630,7 +77933,7 @@ function register7(server) {
77630
77933
  return err(e);
77631
77934
  }
77632
77935
  });
77633
- 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 }) => {
77936
+ 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 }) => {
77634
77937
  try {
77635
77938
  const { createWatchJob: createWatchJob2 } = await Promise.resolve().then(() => (init_url_watcher(), exports_url_watcher));
77636
77939
  return json(createWatchJob2(url, schedule, { name, selector }));
@@ -77662,7 +77965,7 @@ function register7(server) {
77662
77965
  return err(e);
77663
77966
  }
77664
77967
  });
77665
- 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 }) => {
77968
+ 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 }) => {
77666
77969
  try {
77667
77970
  const sid = resolveSessionId(session_id);
77668
77971
  const page = getSessionPage(sid);
@@ -77674,6 +77977,13 @@ function register7(server) {
77674
77977
  });
77675
77978
  }
77676
77979
 
77980
+ // src/mcp/meta.ts
77981
+ function register7(server) {
77982
+ registerAgentsAndProjects(server);
77983
+ registerGalleryAndDownloads(server);
77984
+ registerIntegrationAndMeta(server);
77985
+ }
77986
+
77677
77987
  // src/mcp/data.ts
77678
77988
  function register8(server) {
77679
77989
  register5(server);
@@ -77682,6 +77992,10 @@ function register8(server) {
77682
77992
  }
77683
77993
 
77684
77994
  // src/mcp/tui.ts
77995
+ init_tui();
77996
+ init_session();
77997
+ var DEFAULT_TOOL_TIMEOUT_MS2 = 15000;
77998
+ var RECONNECT_ON_STUCK = true;
77685
77999
  var KEY_MAP = {
77686
78000
  "ctrl+c": "\x03",
77687
78001
  "ctrl+d": "\x04",
@@ -77728,34 +78042,76 @@ var KEY_MAP = {
77728
78042
  f12: "F12"
77729
78043
  };
77730
78044
  function assertTuiSession(sessionId) {
77731
- const { getSessionEngine: getSessionEngine2 } = (init_session(), __toCommonJS(exports_session));
77732
- const engine = getSessionEngine2(sessionId);
78045
+ const engine = getSessionEngine(sessionId);
77733
78046
  if (engine !== "tui") {
77734
- 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")`);
78047
+ 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")`);
77735
78048
  }
77736
78049
  }
77737
- async function getTermText(page, startRow, endRow) {
77738
- const result = await page.evaluate((args) => {
77739
- const [sr, er] = args;
77740
- const term = window.term ?? window.terminal;
77741
- if (!term?.buffer?.active)
77742
- return { text: "", rows: [], row_count: 0 };
77743
- const buf = term.buffer.active;
77744
- const allRows = [];
77745
- for (let i = 0;i < buf.length; i++) {
77746
- const line = buf.getLine(i);
77747
- if (line)
77748
- allRows.push(line.translateToString(true));
77749
- }
77750
- const start = sr ?? 0;
77751
- const end = er ?? allRows.length;
77752
- const filtered = allRows.slice(start, end);
77753
- return { text: filtered.join(`
77754
- `).trimEnd(), rows: filtered, row_count: allRows.length };
77755
- }, [startRow, endRow]);
77756
- return result;
78050
+ function getTuiSession(sessionId) {
78051
+ return getSessionTuiSession(sessionId);
78052
+ }
78053
+ function getTuiMeta(sessionId) {
78054
+ const session = getTuiSession(sessionId);
78055
+ return {
78056
+ method: session.method,
78057
+ reconnected: session.reconnectCount > 0
78058
+ };
78059
+ }
78060
+ function withMeta(sessionId, data) {
78061
+ return { ...data, ...getTuiMeta(sessionId) };
78062
+ }
78063
+ function withStableMeta(sessionId, data) {
78064
+ return { ...data, stuck: false, ...getTuiMeta(sessionId) };
78065
+ }
78066
+ function filterRows(rows, startRow, endRow) {
78067
+ const start = startRow ?? 0;
78068
+ const end = endRow ?? rows.length;
78069
+ const filtered = rows.slice(start, end);
78070
+ return {
78071
+ text: filtered.join(`
78072
+ `).trimEnd(),
78073
+ rows: filtered
78074
+ };
77757
78075
  }
77758
78076
  var activeRecordings2 = new Map;
78077
+ async function withTuiHealth(sessionId, operation, options = {}) {
78078
+ const {
78079
+ timeoutMs = DEFAULT_TOOL_TIMEOUT_MS2,
78080
+ reconnectOnStuck = RECONNECT_ON_STUCK,
78081
+ operationName = "operation"
78082
+ } = options;
78083
+ let session = getTuiSession(sessionId);
78084
+ let page = getSessionPage(sessionId);
78085
+ const health = await isTuiHealthy(session);
78086
+ if (!health.healthy && reconnectOnStuck && session.reconnectCount < 2) {
78087
+ try {
78088
+ const { getSessionCommand: getSessionCommand2, setSessionTui: setSessionTui2 } = await Promise.resolve().then(() => (init_session(), exports_session));
78089
+ const cmd = getSessionCommand2?.(sessionId) ?? "bash";
78090
+ const newSession = await reconnectTui(session, cmd, { method: session.method });
78091
+ setSessionTui2(sessionId, newSession);
78092
+ session = newSession;
78093
+ page = newSession.page;
78094
+ } catch {}
78095
+ } else if (!health.healthy) {
78096
+ throw Object.assign(new Error(`TUI session is unhealthy: ${health.reason}. Close and reopen the session.`), { code: "TUI_UNHEALTHY" });
78097
+ }
78098
+ let timedOut = false;
78099
+ const timer = setTimeout(() => {
78100
+ timedOut = true;
78101
+ }, timeoutMs);
78102
+ try {
78103
+ return await operation(page, session);
78104
+ } catch (error) {
78105
+ if (timedOut) {
78106
+ 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.`);
78107
+ Object.assign(err2, { code: "TUI_TIMEOUT" });
78108
+ throw err2;
78109
+ }
78110
+ throw error;
78111
+ } finally {
78112
+ clearTimeout(timer);
78113
+ }
78114
+ }
77759
78115
  function register9(server) {
77760
78116
  server.tool("browser_tui_send_keys", `Send keystrokes to a TUI terminal session. Use friendly key names.
77761
78117
 
@@ -77769,103 +78125,123 @@ SUPPORTED KEYS:
77769
78125
  Pass multiple keys as a comma-separated string: "tab,tab,enter" or "ctrl+c"
77770
78126
  For typing text, use browser_tui_send_text instead.`, {
77771
78127
  session_id: exports_external2.string().optional(),
77772
- keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'")
77773
- }, async ({ session_id, keys }) => {
78128
+ keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'"),
78129
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78130
+ }, async ({ session_id, keys, timeout_ms }) => {
77774
78131
  try {
77775
78132
  const sid = resolveSessionId(session_id);
77776
78133
  assertTuiSession(sid);
77777
- const page = getSessionPage(sid);
77778
- const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
77779
- const sent = [];
77780
- for (const key of keyList) {
77781
- const mapped = KEY_MAP[key];
77782
- if (mapped) {
77783
- if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
77784
- await page.keyboard.insertText(mapped);
78134
+ const result = await withTuiHealth(sid, async (page) => {
78135
+ const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
78136
+ const sent = [];
78137
+ for (const key of keyList) {
78138
+ const mapped = KEY_MAP[key];
78139
+ if (mapped) {
78140
+ if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
78141
+ await page.keyboard.insertText(mapped);
78142
+ } else {
78143
+ await page.keyboard.press(mapped);
78144
+ }
78145
+ sent.push(key);
77785
78146
  } else {
77786
- await page.keyboard.press(mapped);
78147
+ await page.keyboard.press(key);
78148
+ sent.push(key);
77787
78149
  }
77788
- sent.push(key);
77789
- } else {
77790
- await page.keyboard.press(key);
77791
- sent.push(key);
77792
78150
  }
77793
- }
77794
- return json({ sent, count: sent.length });
78151
+ return { sent, count: sent.length };
78152
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_send_keys" });
78153
+ return json(withStableMeta(sid, result));
77795
78154
  } catch (e) {
78155
+ if (e.code === "TUI_TIMEOUT")
78156
+ return err(e);
78157
+ if (e.code === "TUI_UNHEALTHY")
78158
+ return err(e);
77796
78159
  return err(e);
77797
78160
  }
77798
78161
  });
77799
- 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.
77800
-
77801
- Examples:
77802
- - Send a command: text="ls -la", press_enter=true
77803
- - Type without executing: text="partial input", press_enter=false
77804
- - Send to a prompt: text="yes", press_enter=true`, {
78162
+ 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.`, {
77805
78163
  session_id: exports_external2.string().optional(),
77806
78164
  text: exports_external2.string().describe("Text to type into the terminal"),
77807
- press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)")
77808
- }, async ({ session_id, text, press_enter }) => {
78165
+ press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)"),
78166
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78167
+ }, async ({ session_id, text, press_enter, timeout_ms }) => {
77809
78168
  try {
77810
78169
  const sid = resolveSessionId(session_id);
77811
78170
  assertTuiSession(sid);
77812
- const page = getSessionPage(sid);
77813
- const textarea = await page.$(".xterm-helper-textarea");
77814
- if (textarea) {
77815
- await textarea.type(text);
77816
- } else {
77817
- await page.keyboard.type(text);
77818
- }
77819
- if (press_enter) {
77820
- await page.keyboard.press("Enter");
77821
- }
77822
- return json({ typed: text, pressed_enter: press_enter });
78171
+ const result = await withTuiHealth(sid, async (page) => {
78172
+ const textarea = await page.$(".xterm-helper-textarea");
78173
+ if (textarea) {
78174
+ await textarea.type(text);
78175
+ } else {
78176
+ await page.keyboard.type(text);
78177
+ }
78178
+ if (press_enter)
78179
+ await page.keyboard.press("Enter");
78180
+ return { typed: text, pressed_enter: press_enter };
78181
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_send_text" });
78182
+ return json(withStableMeta(sid, result));
77823
78183
  } catch (e) {
78184
+ if (e.code === "TUI_TIMEOUT")
78185
+ return err(e);
78186
+ if (e.code === "TUI_UNHEALTHY")
78187
+ return err(e);
77824
78188
  return err(e);
77825
78189
  }
77826
78190
  });
77827
- 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.", {
78191
+ server.tool("browser_tui_resize", "Resize the terminal to a specific number of columns and rows.", {
77828
78192
  session_id: exports_external2.string().optional(),
77829
78193
  cols: exports_external2.number().describe("Number of columns (e.g. 80, 120, 200)"),
77830
- rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)")
77831
- }, async ({ session_id, cols, rows }) => {
78194
+ rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)"),
78195
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78196
+ }, async ({ session_id, cols, rows, timeout_ms }) => {
77832
78197
  try {
77833
78198
  const sid = resolveSessionId(session_id);
77834
78199
  assertTuiSession(sid);
77835
- const page = getSessionPage(sid);
77836
- const result = await page.evaluate((args) => {
77837
- const [c, r] = args;
77838
- const term = window.term ?? window.terminal;
77839
- if (!term)
77840
- return { resized: false, error: "No terminal instance found" };
77841
- term.resize(c, r);
77842
- return { resized: true, cols: c, rows: r };
77843
- }, [cols, rows]);
77844
- return json(result);
78200
+ const result = await withTuiHealth(sid, async (page) => {
78201
+ return page.evaluate((args) => {
78202
+ const [c, r] = args;
78203
+ const term = window.term ?? window.terminal;
78204
+ if (!term)
78205
+ return { resized: false, error: "No terminal instance found" };
78206
+ term.resize(c, r);
78207
+ return { resized: true, cols: c, rows: r };
78208
+ }, [cols, rows]);
78209
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_resize" });
78210
+ return json(withMeta(sid, result));
77845
78211
  } catch (e) {
78212
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
78213
+ return err(e);
77846
78214
  return err(e);
77847
78215
  }
77848
78216
  });
77849
- server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.
77850
-
77851
- Use this to read what the terminal is currently displaying. For waiting until specific text appears, use browser_tui_wait_for_text instead.`, {
78217
+ server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.`, {
77852
78218
  session_id: exports_external2.string().optional(),
77853
78219
  start_row: exports_external2.number().optional().describe("First row to read (0-indexed, default: 0)"),
77854
- end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows.")
77855
- }, async ({ session_id, start_row, end_row }) => {
78220
+ end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows."),
78221
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78222
+ }, async ({ session_id, start_row, end_row, timeout_ms }) => {
77856
78223
  try {
77857
78224
  const sid = resolveSessionId(session_id);
77858
78225
  assertTuiSession(sid);
77859
- const page = getSessionPage(sid);
77860
- const result = await getTermText(page, start_row, end_row);
77861
- return json(result);
78226
+ const result = await withTuiHealth(sid, async (page, session) => {
78227
+ const state = await getTerminalState(page, session.method, timeout_ms);
78228
+ const filtered = filterRows(state.rows, start_row, end_row);
78229
+ return {
78230
+ ...filtered,
78231
+ row_count: state.row_count
78232
+ };
78233
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_get_text" });
78234
+ return json(withMeta(sid, result));
77862
78235
  } catch (e) {
78236
+ if (e.code === "TUI_TIMEOUT")
78237
+ return err(e);
78238
+ if (e.code === "TUI_UNHEALTHY")
78239
+ return err(e);
77863
78240
  return err(e);
77864
78241
  }
77865
78242
  });
77866
- 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.
77867
-
77868
- Use this after sending a command to wait for its output, or to wait for a TUI app to finish loading.`, {
78243
+ server.tool("browser_tui_wait_for_text", `Wait for specific text to appear in the terminal output. Polls until found or timeout.
78244
+ Returns stuck:true if the terminal became unresponsive during the wait.`, {
77869
78245
  session_id: exports_external2.string().optional(),
77870
78246
  text: exports_external2.string().describe("Text to wait for (substring match)"),
77871
78247
  timeout_ms: exports_external2.number().optional().default(30000).describe("Timeout in milliseconds (default: 30000)")
@@ -77873,38 +78249,37 @@ Use this after sending a command to wait for its output, or to wait for a TUI ap
77873
78249
  try {
77874
78250
  const sid = resolveSessionId(session_id);
77875
78251
  assertTuiSession(sid);
77876
- const page = getSessionPage(sid);
77877
- const start = Date.now();
77878
- while (Date.now() - start < timeout_ms) {
77879
- const result = await getTermText(page);
77880
- if (result.text.includes(text)) {
77881
- return json({ found: true, elapsed_ms: Date.now() - start, terminal_text: result.text });
77882
- }
77883
- await new Promise((r) => setTimeout(r, 250));
77884
- }
77885
- const finalText = await getTermText(page);
77886
- return json({ found: false, elapsed_ms: timeout_ms, terminal_text: finalText.text });
78252
+ const result = await withTuiHealth(sid, async (page, session) => {
78253
+ return waitForTerminalText(page, text, timeout_ms, session.method);
78254
+ }, { timeoutMs: timeout_ms + 5000, operationName: "browser_tui_wait_for_text" });
78255
+ return json(withMeta(sid, result));
77887
78256
  } catch (e) {
78257
+ if (e.code === "TUI_TIMEOUT")
78258
+ return err(e);
78259
+ if (e.code === "TUI_UNHEALTHY")
78260
+ return err(e);
77888
78261
  return err(e);
77889
78262
  }
77890
78263
  });
77891
78264
  server.tool("browser_tui_get_cursor", "Get the current cursor position (row and column) in the terminal.", {
77892
- session_id: exports_external2.string().optional()
77893
- }, async ({ session_id }) => {
78265
+ session_id: exports_external2.string().optional(),
78266
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78267
+ }, async ({ session_id, timeout_ms }) => {
77894
78268
  try {
77895
78269
  const sid = resolveSessionId(session_id);
77896
78270
  assertTuiSession(sid);
77897
- const page = getSessionPage(sid);
77898
- const cursor = await page.evaluate(() => {
77899
- const term = window.term ?? window.terminal;
77900
- if (!term?.buffer?.active)
78271
+ const result = await withTuiHealth(sid, async (page, session) => {
78272
+ const state = await getTerminalState(page, session.method, timeout_ms);
78273
+ if (state.cursor_row < 0 || state.cursor_col < 0)
77901
78274
  return null;
77902
- return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
77903
- });
77904
- if (!cursor)
77905
- return err(new Error("Could not read cursor position \u2014 no terminal instance"));
77906
- return json(cursor);
78275
+ return { row: state.cursor_row, col: state.cursor_col };
78276
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_get_cursor" });
78277
+ if (!result)
78278
+ return err(new Error("Could not read cursor \u2014 no terminal instance"));
78279
+ return json(withStableMeta(sid, result));
77907
78280
  } catch (e) {
78281
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
78282
+ return err(e);
77908
78283
  return err(e);
77909
78284
  }
77910
78285
  });
@@ -77915,140 +78290,135 @@ CONDITION SYNTAX:
77915
78290
  - "row N contains X" \u2014 row N (0-indexed) contains substring X
77916
78291
  - "cursor at R,C" \u2014 cursor is at row R, column C
77917
78292
  - "row_count > N" \u2014 total rows greater than N
77918
- - "row_count == N" \u2014 total rows equals N
77919
-
77920
- Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
78293
+ - "row_count == N" \u2014 total rows equals N`, {
77921
78294
  session_id: exports_external2.string().optional(),
77922
- condition: exports_external2.string().describe("Assertion condition(s), joined with AND")
77923
- }, async ({ session_id, condition }) => {
78295
+ condition: exports_external2.string().describe("Assertion condition(s), joined with AND"),
78296
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78297
+ }, async ({ session_id, condition, timeout_ms }) => {
77924
78298
  try {
77925
78299
  const sid = resolveSessionId(session_id);
77926
78300
  assertTuiSession(sid);
77927
- const page = getSessionPage(sid);
77928
- const termData = await getTermText(page);
77929
- const cursor = await page.evaluate(() => {
77930
- const term = window.term ?? window.terminal;
77931
- if (!term?.buffer?.active)
77932
- return { row: -1, col: -1 };
77933
- return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
77934
- });
77935
- const checks = [];
77936
- let allPassed = true;
77937
- for (const part of condition.split(/\s+AND\s+/i)) {
77938
- const trimmed = part.trim();
77939
- let result = false;
77940
- if (/^text\s+contains\s+/i.test(trimmed)) {
77941
- const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
77942
- result = termData.text.includes(needle);
77943
- } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
77944
- const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
77945
- if (match) {
77946
- const rowIdx = parseInt(match[1]);
77947
- const needle = match[2].replace(/^["']|["']$/g, "");
77948
- result = (termData.rows[rowIdx] ?? "").includes(needle);
77949
- }
77950
- } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
77951
- const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
77952
- if (match) {
77953
- result = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
77954
- }
77955
- } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
77956
- const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
77957
- if (match) {
77958
- const op = match[1];
77959
- const n = parseInt(match[2]);
77960
- const count = termData.row_count;
77961
- result = op === ">" ? count > n : op === ">=" ? count >= n : op === "<" ? count < n : op === "<=" ? count <= n : op === "==" ? count === n : count !== n;
77962
- }
77963
- }
77964
- checks.push({ assertion: trimmed, result });
77965
- if (!result)
77966
- allPassed = false;
77967
- }
77968
- return json({ passed: allPassed, checks, cursor, row_count: termData.row_count });
78301
+ const result = await withTuiHealth(sid, async (page, session) => {
78302
+ const state = await getTerminalState(page, session.method, timeout_ms);
78303
+ const termText = state.text;
78304
+ const cursor = { row: state.cursor_row, col: state.cursor_col };
78305
+ const checks = [];
78306
+ let allPassed = true;
78307
+ for (const part of condition.split(/\s+AND\s+/i)) {
78308
+ const trimmed = part.trim();
78309
+ let passed = false;
78310
+ if (/^text\s+contains\s+/i.test(trimmed)) {
78311
+ const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
78312
+ passed = termText.includes(needle);
78313
+ } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
78314
+ const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
78315
+ if (match) {
78316
+ const rowIdx = parseInt(match[1]);
78317
+ const needle = match[2].replace(/^["']|["']$/g, "");
78318
+ passed = (state.rows[rowIdx] ?? "").includes(needle);
78319
+ }
78320
+ } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
78321
+ const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
78322
+ if (match)
78323
+ passed = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
78324
+ } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
78325
+ const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
78326
+ if (match) {
78327
+ const op = match[1];
78328
+ const n = parseInt(match[2]);
78329
+ const cnt = state.row_count;
78330
+ passed = op === ">" ? cnt > n : op === ">=" ? cnt >= n : op === "<" ? cnt < n : op === "<=" ? cnt <= n : op === "==" ? cnt === n : cnt !== n;
78331
+ }
78332
+ }
78333
+ checks.push({ assertion: trimmed, result: passed });
78334
+ if (!passed)
78335
+ allPassed = false;
78336
+ }
78337
+ return { passed: allPassed, checks, cursor, row_count: state.row_count };
78338
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_assert" });
78339
+ return json(withMeta(sid, result));
77969
78340
  } catch (e) {
78341
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
78342
+ return err(e);
77970
78343
  return err(e);
77971
78344
  }
77972
78345
  });
77973
- 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.", {
77974
- session_id: exports_external2.string().optional()
77975
- }, async ({ session_id }) => {
78346
+ server.tool("browser_tui_snapshot", "Capture a structured snapshot of the terminal buffer: all rows, row refs, cursor position, dimensions, and theme.", {
78347
+ session_id: exports_external2.string().optional(),
78348
+ timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
78349
+ }, async ({ session_id, timeout_ms }) => {
77976
78350
  try {
77977
78351
  const sid = resolveSessionId(session_id);
77978
78352
  assertTuiSession(sid);
77979
- const page = getSessionPage(sid);
77980
- const snapshot = await page.evaluate(() => {
77981
- const term = window.term ?? window.terminal;
77982
- if (!term?.buffer?.active)
77983
- return null;
77984
- const buf = term.buffer.active;
77985
- const rows = [];
77986
- for (let i = 0;i < buf.length; i++) {
77987
- const line = buf.getLine(i);
77988
- if (line)
77989
- rows.push(line.translateToString(true));
77990
- }
78353
+ const result = await withTuiHealth(sid, async (page, session) => {
78354
+ const state = await getTerminalState(page, session.method, timeout_ms);
77991
78355
  return {
77992
- rows,
77993
- cols: term.cols,
77994
- total_rows: term.rows,
77995
- buffer_length: buf.length,
77996
- cursor_row: buf.cursorY,
77997
- cursor_col: buf.cursorX,
77998
- font_size: term.options?.fontSize,
77999
- theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
78356
+ rows: state.rows,
78357
+ refs: state.refs,
78358
+ cols: state.cols,
78359
+ total_rows: state.total_rows,
78360
+ buffer_length: state.buffer_length,
78361
+ cursor_row: state.cursor_row,
78362
+ cursor_col: state.cursor_col,
78363
+ font_size: state.font_size,
78364
+ theme: state.theme
78000
78365
  };
78001
- });
78002
- if (!snapshot)
78003
- return err(new Error("Could not capture snapshot \u2014 no terminal instance"));
78004
- return json(snapshot);
78366
+ }, { timeoutMs: timeout_ms, operationName: "browser_tui_snapshot" });
78367
+ return json(withStableMeta(sid, result));
78005
78368
  } catch (e) {
78369
+ if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
78370
+ return err(e);
78006
78371
  return err(e);
78007
78372
  }
78008
78373
  });
78009
- 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.", {
78374
+ server.tool("browser_tui_record_start", "Start recording the terminal session as an asciicast v2 file.", {
78010
78375
  session_id: exports_external2.string().optional(),
78011
78376
  interval_ms: exports_external2.number().optional().default(500).describe("Polling interval in ms (default: 500)")
78012
78377
  }, async ({ session_id, interval_ms }) => {
78013
78378
  try {
78014
78379
  const sid = resolveSessionId(session_id);
78015
78380
  assertTuiSession(sid);
78016
- const page = getSessionPage(sid);
78017
78381
  if (activeRecordings2.has(sid)) {
78018
78382
  return err(new Error("Recording already active for this session. Stop it first with browser_tui_record_stop."));
78019
78383
  }
78020
- const dims = await page.evaluate(() => {
78021
- const term = window.term ?? window.terminal;
78022
- return term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
78023
- });
78024
- const initialText = (await getTermText(page)).text;
78384
+ const page = getSessionPage(sid);
78385
+ const session = getTuiSession(sid);
78386
+ const initialState = await getTerminalState(page, session.method);
78387
+ const dims = { cols: initialState.cols ?? 80, rows: initialState.total_rows || initialState.row_count || 24 };
78025
78388
  const recording = {
78026
78389
  sessionId: sid,
78027
78390
  startTime: Date.now(),
78028
78391
  cols: dims.cols,
78029
78392
  rows: dims.rows,
78030
78393
  events: [],
78031
- lastText: initialText,
78394
+ lastText: initialState.text,
78032
78395
  intervalId: setInterval(async () => {
78033
78396
  try {
78034
- const current = await getTermText(page);
78035
- if (current.text !== recording.lastText) {
78397
+ const currentPage = getSessionPage(sid);
78398
+ const currentSession = getTuiSession(sid);
78399
+ const state = await getTerminalState(currentPage, currentSession.method);
78400
+ if (state.text !== recording.lastText) {
78036
78401
  const elapsed = (Date.now() - recording.startTime) / 1000;
78037
- recording.events.push([elapsed, "o", current.text.slice(recording.lastText.length) || current.text]);
78038
- recording.lastText = current.text;
78402
+ recording.events.push([elapsed, "o", state.text.slice(recording.lastText.length) || state.text]);
78403
+ recording.lastText = state.text;
78039
78404
  }
78040
78405
  } catch {}
78041
78406
  }, interval_ms)
78042
78407
  };
78043
78408
  activeRecordings2.set(sid, recording);
78044
- return json({ recording: true, session_id: sid, interval_ms, cols: dims.cols, rows: dims.rows });
78409
+ return json({
78410
+ recording: true,
78411
+ session_id: sid,
78412
+ interval_ms,
78413
+ cols: dims.cols,
78414
+ rows: dims.rows,
78415
+ method: session.method
78416
+ });
78045
78417
  } catch (e) {
78046
78418
  return err(e);
78047
78419
  }
78048
78420
  });
78049
- server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON. Compatible with asciinema player.", {
78050
- session_id: exports_external2.string().optional()
78051
- }, async ({ session_id }) => {
78421
+ server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON.", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
78052
78422
  try {
78053
78423
  const sid = resolveSessionId(session_id);
78054
78424
  const recording = activeRecordings2.get(sid);
@@ -78066,16 +78436,34 @@ Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
78066
78436
  env: { TERM: "xterm-256color", SHELL: "/bin/bash" }
78067
78437
  };
78068
78438
  const lines = [JSON.stringify(header)];
78069
- for (const [time, type2, data] of recording.events) {
78439
+ for (const [time, type2, data] of recording.events)
78070
78440
  lines.push(JSON.stringify([time, type2, data]));
78071
- }
78072
78441
  const asciicast = lines.join(`
78073
78442
  `);
78074
78443
  return json({
78075
78444
  format: "asciicast_v2",
78076
78445
  duration_seconds: Math.round(duration * 10) / 10,
78077
78446
  event_count: recording.events.length,
78078
- asciicast
78447
+ asciicast,
78448
+ method: getTuiSession(sid).method
78449
+ });
78450
+ } catch (e) {
78451
+ return err(e);
78452
+ }
78453
+ });
78454
+ server.tool("browser_tui_health", `Health check for a TUI session. Returns healthy status, latency, reconnect count, and the active read method.
78455
+ Use this to verify a session is still responsive before running other tools.`, { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
78456
+ try {
78457
+ const sid = resolveSessionId(session_id);
78458
+ assertTuiSession(sid);
78459
+ const session = getTuiSession(sid);
78460
+ const health = await isTuiHealthy(session);
78461
+ return json({
78462
+ healthy: health.healthy,
78463
+ latency_ms: health.healthy ? health.latency_ms : null,
78464
+ reason: health.healthy ? null : health.reason,
78465
+ reconnect_count: session.reconnectCount,
78466
+ method: session.method
78079
78467
  });
78080
78468
  } catch (e) {
78081
78469
  return err(e);