@hasna/browser 0.4.1 → 0.4.3

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
@@ -10657,7 +10657,42 @@ async function launchTui(command, options = {}) {
10657
10657
  waitUntil: "domcontentloaded"
10658
10658
  });
10659
10659
  await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
10660
- return { ttydProcess, port, browser, page };
10660
+ let resolvedTheme = "dark";
10661
+ const requestedTheme = options.theme ?? "system";
10662
+ if (requestedTheme === "light") {
10663
+ resolvedTheme = "light";
10664
+ } else if (requestedTheme === "dark") {
10665
+ resolvedTheme = "dark";
10666
+ } else {
10667
+ try {
10668
+ const result = execSync2("defaults read -g AppleInterfaceStyle 2>/dev/null", { encoding: "utf8" }).trim();
10669
+ resolvedTheme = result === "Dark" ? "dark" : "light";
10670
+ } catch {
10671
+ resolvedTheme = "light";
10672
+ }
10673
+ }
10674
+ const themeColors = THEMES[resolvedTheme];
10675
+ await page.evaluate((theme) => {
10676
+ const term = window.term ?? window.terminal;
10677
+ if (term?.options) {
10678
+ term.options.theme = theme;
10679
+ }
10680
+ document.body.style.backgroundColor = theme.background;
10681
+ const container = document.getElementById("terminal-container");
10682
+ if (container)
10683
+ container.style.backgroundColor = theme.background;
10684
+ const viewport2 = document.querySelector(".xterm-viewport");
10685
+ if (viewport2)
10686
+ viewport2.style.backgroundColor = theme.background;
10687
+ }, themeColors);
10688
+ if (options.fontSize) {
10689
+ await page.evaluate((size) => {
10690
+ const term = window.term ?? window.terminal;
10691
+ if (term?.options)
10692
+ term.options.fontSize = size;
10693
+ }, options.fontSize);
10694
+ }
10695
+ return { ttydProcess, port, browser, page, theme: resolvedTheme };
10661
10696
  } catch (err) {
10662
10697
  ttydProcess.kill();
10663
10698
  throw err;
@@ -10674,11 +10709,57 @@ async function closeTui(session) {
10674
10709
  session.ttydProcess.kill("SIGTERM");
10675
10710
  } catch {}
10676
10711
  }
10677
- var DEFAULT_TTYD_PORT_START = 7780, nextPort;
10712
+ var DEFAULT_TTYD_PORT_START = 7780, nextPort, THEMES;
10678
10713
  var init_tui = __esm(() => {
10679
10714
  init_types();
10680
10715
  init_playwright();
10681
10716
  nextPort = DEFAULT_TTYD_PORT_START;
10717
+ THEMES = {
10718
+ dark: {
10719
+ background: "#1e1e1e",
10720
+ foreground: "#d4d4d4",
10721
+ cursor: "#d4d4d4",
10722
+ selectionBackground: "#264f78",
10723
+ black: "#1e1e1e",
10724
+ red: "#f44747",
10725
+ green: "#6a9955",
10726
+ yellow: "#d7ba7d",
10727
+ blue: "#569cd6",
10728
+ magenta: "#c586c0",
10729
+ cyan: "#4ec9b0",
10730
+ white: "#d4d4d4",
10731
+ brightBlack: "#808080",
10732
+ brightRed: "#f44747",
10733
+ brightGreen: "#6a9955",
10734
+ brightYellow: "#d7ba7d",
10735
+ brightBlue: "#569cd6",
10736
+ brightMagenta: "#c586c0",
10737
+ brightCyan: "#4ec9b0",
10738
+ brightWhite: "#ffffff"
10739
+ },
10740
+ light: {
10741
+ background: "#ffffff",
10742
+ foreground: "#1e1e1e",
10743
+ cursor: "#1e1e1e",
10744
+ selectionBackground: "#add6ff",
10745
+ black: "#1e1e1e",
10746
+ red: "#cd3131",
10747
+ green: "#008000",
10748
+ yellow: "#795e26",
10749
+ blue: "#0451a5",
10750
+ magenta: "#af00db",
10751
+ cyan: "#0598bc",
10752
+ white: "#d4d4d4",
10753
+ brightBlack: "#808080",
10754
+ brightRed: "#cd3131",
10755
+ brightGreen: "#008000",
10756
+ brightYellow: "#795e26",
10757
+ brightBlue: "#0451a5",
10758
+ brightMagenta: "#af00db",
10759
+ brightCyan: "#0598bc",
10760
+ brightWhite: "#ffffff"
10761
+ }
10762
+ };
10682
10763
  });
10683
10764
 
10684
10765
  // src/engines/selector.ts
@@ -12111,7 +12192,9 @@ async function createSession2(opts = {}) {
12111
12192
  const command = opts.startUrl ?? "bash";
12112
12193
  const tuiSess = await launchTui(command, {
12113
12194
  headless: opts.headless ?? true,
12114
- viewport: opts.viewport
12195
+ viewport: opts.viewport,
12196
+ theme: opts.tuiTheme ?? "system",
12197
+ fontSize: opts.tuiFontSize
12115
12198
  });
12116
12199
  browser = tuiSess.browser;
12117
12200
  page = tuiSess.page;
@@ -43815,12 +43898,26 @@ function resolveSessionId(sessionId) {
43815
43898
 
43816
43899
  // src/mcp/sessions.ts
43817
43900
  function register(server) {
43818
- server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected. Use cdp_url to attach to an already-running Chrome instance.", {
43819
- engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "tui", "auto"]).optional().default("auto"),
43820
- use_case: exports_external.string().optional(),
43901
+ server.tool("browser_session_create", `Create a new browser session. Returns a session object with an id you must pass to other tools.
43902
+
43903
+ ENGINES:
43904
+ - "auto" (default): picks the best engine for your use case automatically
43905
+ - "playwright": full browser automation \u2014 forms, SPAs, auth flows, multi-tab
43906
+ - "cdp": Chrome DevTools Protocol \u2014 network monitoring, perf profiling, script injection
43907
+ - "lightpanda": fast headless for static pages
43908
+ - "bun": native Bun.WebView \u2014 fastest for screenshots and scraping
43909
+ - "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.
43910
+
43911
+ TIPS:
43912
+ - If agent_id is set and already has an active session, returns the existing one (use force_new to override)
43913
+ - If session_id is omitted on other tools, the single active session is auto-selected
43914
+ - Use cdp_url to attach to an already-running Chrome instance
43915
+ - For TUI sessions: start_url is the shell command to run, NOT a URL`, {
43916
+ engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "tui", "auto"]).optional().default("auto").describe("Browser engine. Use 'tui' for terminal/CLI app testing \u2014 pass the command as start_url"),
43917
+ use_case: exports_external.string().optional().describe("Hint for auto engine selection: scrape, screenshot, form, auth, network, har, perf, terminal, tui"),
43821
43918
  project_id: exports_external.string().optional(),
43822
43919
  agent_id: exports_external.string().optional(),
43823
- start_url: exports_external.string().optional(),
43920
+ start_url: exports_external.string().optional().describe("URL to navigate to, OR for engine='tui': the shell command to run (e.g. 'htop', 'bun run app.tsx')"),
43824
43921
  headless: exports_external.boolean().optional().default(true),
43825
43922
  viewport_width: exports_external.number().optional().default(1280),
43826
43923
  viewport_height: exports_external.number().optional().default(720),
@@ -43829,8 +43926,10 @@ function register(server) {
43829
43926
  storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
43830
43927
  force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
43831
43928
  tags: exports_external.array(exports_external.string()).optional(),
43832
- cdp_url: exports_external.string().optional().describe("Connect to existing Chrome via CDP (e.g. http://localhost:9222). Start Chrome with --remote-debugging-port=9222")
43833
- }, 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 }) => {
43929
+ cdp_url: exports_external.string().optional().describe("Connect to existing Chrome via CDP (e.g. http://localhost:9222). Start Chrome with --remote-debugging-port=9222"),
43930
+ tui_theme: exports_external.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."),
43931
+ tui_font_size: exports_external.number().optional().default(14).describe("TUI engine only: terminal font size in pixels (default: 14). Larger = more readable screenshots, smaller = more content visible.")
43932
+ }, 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 }) => {
43834
43933
  try {
43835
43934
  if (agent_id && !force_new) {
43836
43935
  const existing = getActiveSessionForAgent2(agent_id);
@@ -43848,7 +43947,9 @@ function register(server) {
43848
43947
  stealth,
43849
43948
  autoGallery: auto_gallery,
43850
43949
  storageState: storage_state,
43851
- cdpUrl: cdp_url
43950
+ cdpUrl: cdp_url,
43951
+ tuiTheme: tui_theme,
43952
+ tuiFontSize: tui_font_size
43852
43953
  });
43853
43954
  if (tags?.length) {
43854
43955
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -46027,6 +46128,18 @@ function register7(server) {
46027
46128
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
46028
46129
  { tool: "browser_tab_close", description: "Close a tab by index" }
46029
46130
  ],
46131
+ TUI: [
46132
+ { tool: "browser_tui_send_keys", description: "Send keystrokes (ctrl+c, arrow_up, tab, enter, etc.)" },
46133
+ { tool: "browser_tui_send_text", description: "Type text + optional Enter (most common TUI interaction)" },
46134
+ { tool: "browser_tui_resize", description: "Resize terminal cols/rows mid-session" },
46135
+ { tool: "browser_tui_get_text", description: "Get terminal text buffer (full or row range)" },
46136
+ { tool: "browser_tui_wait_for_text", description: "Wait for text to appear in terminal output" },
46137
+ { tool: "browser_tui_get_cursor", description: "Get cursor position (row, col)" },
46138
+ { tool: "browser_tui_assert", description: "Assert terminal conditions (text contains, row N contains, cursor at)" },
46139
+ { tool: "browser_tui_snapshot", description: "Structured terminal snapshot (rows array, cursor, dimensions)" },
46140
+ { tool: "browser_tui_record_start", description: "Start recording terminal as asciicast" },
46141
+ { tool: "browser_tui_record_stop", description: "Stop recording, return asciicast v2 JSON" }
46142
+ ],
46030
46143
  Meta: [
46031
46144
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
46032
46145
  { tool: "browser_version", description: "Show running binary version and tool count" },
@@ -46431,6 +46544,408 @@ function register8(server) {
46431
46544
  register7(server);
46432
46545
  }
46433
46546
 
46547
+ // src/mcp/tui.ts
46548
+ var KEY_MAP = {
46549
+ "ctrl+c": "\x03",
46550
+ "ctrl+d": "\x04",
46551
+ "ctrl+z": "\x1A",
46552
+ "ctrl+l": "\f",
46553
+ "ctrl+a": "\x01",
46554
+ "ctrl+e": "\x05",
46555
+ "ctrl+k": "\v",
46556
+ "ctrl+u": "\x15",
46557
+ "ctrl+w": "\x17",
46558
+ "ctrl+r": "\x12",
46559
+ "ctrl+p": "\x10",
46560
+ "ctrl+n": "\x0E",
46561
+ enter: "Enter",
46562
+ tab: "Tab",
46563
+ escape: "Escape",
46564
+ esc: "Escape",
46565
+ backspace: "Backspace",
46566
+ delete: "Delete",
46567
+ space: " ",
46568
+ up: "ArrowUp",
46569
+ down: "ArrowDown",
46570
+ left: "ArrowLeft",
46571
+ right: "ArrowRight",
46572
+ arrow_up: "ArrowUp",
46573
+ arrow_down: "ArrowDown",
46574
+ arrow_left: "ArrowLeft",
46575
+ arrow_right: "ArrowRight",
46576
+ home: "Home",
46577
+ end: "End",
46578
+ page_up: "PageUp",
46579
+ page_down: "PageDown",
46580
+ f1: "F1",
46581
+ f2: "F2",
46582
+ f3: "F3",
46583
+ f4: "F4",
46584
+ f5: "F5",
46585
+ f6: "F6",
46586
+ f7: "F7",
46587
+ f8: "F8",
46588
+ f9: "F9",
46589
+ f10: "F10",
46590
+ f11: "F11",
46591
+ f12: "F12"
46592
+ };
46593
+ function assertTuiSession(sessionId) {
46594
+ const { getSessionEngine: getSessionEngine2 } = (init_session(), __toCommonJS(exports_session));
46595
+ const engine = getSessionEngine2(sessionId);
46596
+ if (engine !== "tui") {
46597
+ 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")`);
46598
+ }
46599
+ }
46600
+ async function getTermText(page, startRow, endRow) {
46601
+ const result = await page.evaluate((args) => {
46602
+ const [sr, er] = args;
46603
+ const term = window.term ?? window.terminal;
46604
+ if (!term?.buffer?.active)
46605
+ return { text: "", rows: [], row_count: 0 };
46606
+ const buf = term.buffer.active;
46607
+ const allRows = [];
46608
+ for (let i = 0;i < buf.length; i++) {
46609
+ const line = buf.getLine(i);
46610
+ if (line)
46611
+ allRows.push(line.translateToString(true));
46612
+ }
46613
+ const start = sr ?? 0;
46614
+ const end = er ?? allRows.length;
46615
+ const filtered = allRows.slice(start, end);
46616
+ return { text: filtered.join(`
46617
+ `).trimEnd(), rows: filtered, row_count: allRows.length };
46618
+ }, [startRow, endRow]);
46619
+ return result;
46620
+ }
46621
+ var activeRecordings2 = new Map;
46622
+ function register9(server) {
46623
+ server.tool("browser_tui_send_keys", `Send keystrokes to a TUI terminal session. Use friendly key names.
46624
+
46625
+ SUPPORTED KEYS:
46626
+ - Control: ctrl+c, ctrl+d, ctrl+z, ctrl+l, ctrl+a, ctrl+e, ctrl+k, ctrl+u, ctrl+w, ctrl+r
46627
+ - Navigation: enter, tab, escape, backspace, delete, space
46628
+ - Arrows: up, down, left, right (or arrow_up, arrow_down, arrow_left, arrow_right)
46629
+ - Function: f1-f12
46630
+ - Position: home, end, page_up, page_down
46631
+
46632
+ Pass multiple keys as a comma-separated string: "tab,tab,enter" or "ctrl+c"
46633
+ For typing text, use browser_tui_send_text instead.`, {
46634
+ session_id: exports_external.string().optional(),
46635
+ keys: exports_external.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'")
46636
+ }, async ({ session_id, keys }) => {
46637
+ try {
46638
+ const sid = resolveSessionId(session_id);
46639
+ assertTuiSession(sid);
46640
+ const page = getSessionPage(sid);
46641
+ const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
46642
+ const sent = [];
46643
+ for (const key of keyList) {
46644
+ const mapped = KEY_MAP[key];
46645
+ if (mapped) {
46646
+ if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
46647
+ await page.keyboard.insertText(mapped);
46648
+ } else {
46649
+ await page.keyboard.press(mapped);
46650
+ }
46651
+ sent.push(key);
46652
+ } else {
46653
+ await page.keyboard.press(key);
46654
+ sent.push(key);
46655
+ }
46656
+ }
46657
+ return json({ sent, count: sent.length });
46658
+ } catch (e) {
46659
+ return err(e);
46660
+ }
46661
+ });
46662
+ 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.
46663
+
46664
+ Examples:
46665
+ - Send a command: text="ls -la", press_enter=true
46666
+ - Type without executing: text="partial input", press_enter=false
46667
+ - Send to a prompt: text="yes", press_enter=true`, {
46668
+ session_id: exports_external.string().optional(),
46669
+ text: exports_external.string().describe("Text to type into the terminal"),
46670
+ press_enter: exports_external.boolean().optional().default(true).describe("Press Enter after typing (default: true)")
46671
+ }, async ({ session_id, text, press_enter }) => {
46672
+ try {
46673
+ const sid = resolveSessionId(session_id);
46674
+ assertTuiSession(sid);
46675
+ const page = getSessionPage(sid);
46676
+ const textarea = await page.$(".xterm-helper-textarea");
46677
+ if (textarea) {
46678
+ await textarea.type(text);
46679
+ } else {
46680
+ await page.keyboard.type(text);
46681
+ }
46682
+ if (press_enter) {
46683
+ await page.keyboard.press("Enter");
46684
+ }
46685
+ return json({ typed: text, pressed_enter: press_enter });
46686
+ } catch (e) {
46687
+ return err(e);
46688
+ }
46689
+ });
46690
+ 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.", {
46691
+ session_id: exports_external.string().optional(),
46692
+ cols: exports_external.number().describe("Number of columns (e.g. 80, 120, 200)"),
46693
+ rows: exports_external.number().describe("Number of rows (e.g. 24, 40, 50)")
46694
+ }, async ({ session_id, cols, rows }) => {
46695
+ try {
46696
+ const sid = resolveSessionId(session_id);
46697
+ assertTuiSession(sid);
46698
+ const page = getSessionPage(sid);
46699
+ const result = await page.evaluate((args) => {
46700
+ const [c, r] = args;
46701
+ const term = window.term ?? window.terminal;
46702
+ if (!term)
46703
+ return { resized: false, error: "No terminal instance found" };
46704
+ term.resize(c, r);
46705
+ return { resized: true, cols: c, rows: r };
46706
+ }, [cols, rows]);
46707
+ return json(result);
46708
+ } catch (e) {
46709
+ return err(e);
46710
+ }
46711
+ });
46712
+ server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.
46713
+
46714
+ Use this to read what the terminal is currently displaying. For waiting until specific text appears, use browser_tui_wait_for_text instead.`, {
46715
+ session_id: exports_external.string().optional(),
46716
+ start_row: exports_external.number().optional().describe("First row to read (0-indexed, default: 0)"),
46717
+ end_row: exports_external.number().optional().describe("Last row (exclusive). Omit for all rows.")
46718
+ }, async ({ session_id, start_row, end_row }) => {
46719
+ try {
46720
+ const sid = resolveSessionId(session_id);
46721
+ assertTuiSession(sid);
46722
+ const page = getSessionPage(sid);
46723
+ const result = await getTermText(page, start_row, end_row);
46724
+ return json(result);
46725
+ } catch (e) {
46726
+ return err(e);
46727
+ }
46728
+ });
46729
+ 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.
46730
+
46731
+ Use this after sending a command to wait for its output, or to wait for a TUI app to finish loading.`, {
46732
+ session_id: exports_external.string().optional(),
46733
+ text: exports_external.string().describe("Text to wait for (substring match)"),
46734
+ timeout_ms: exports_external.number().optional().default(30000).describe("Timeout in milliseconds (default: 30000)")
46735
+ }, async ({ session_id, text, timeout_ms }) => {
46736
+ try {
46737
+ const sid = resolveSessionId(session_id);
46738
+ assertTuiSession(sid);
46739
+ const page = getSessionPage(sid);
46740
+ const start = Date.now();
46741
+ while (Date.now() - start < timeout_ms) {
46742
+ const result = await getTermText(page);
46743
+ if (result.text.includes(text)) {
46744
+ return json({ found: true, elapsed_ms: Date.now() - start, terminal_text: result.text });
46745
+ }
46746
+ await new Promise((r) => setTimeout(r, 250));
46747
+ }
46748
+ const finalText = await getTermText(page);
46749
+ return json({ found: false, elapsed_ms: timeout_ms, terminal_text: finalText.text });
46750
+ } catch (e) {
46751
+ return err(e);
46752
+ }
46753
+ });
46754
+ server.tool("browser_tui_get_cursor", "Get the current cursor position (row and column) in the terminal.", {
46755
+ session_id: exports_external.string().optional()
46756
+ }, async ({ session_id }) => {
46757
+ try {
46758
+ const sid = resolveSessionId(session_id);
46759
+ assertTuiSession(sid);
46760
+ const page = getSessionPage(sid);
46761
+ const cursor = await page.evaluate(() => {
46762
+ const term = window.term ?? window.terminal;
46763
+ if (!term?.buffer?.active)
46764
+ return null;
46765
+ return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
46766
+ });
46767
+ if (!cursor)
46768
+ return err(new Error("Could not read cursor position \u2014 no terminal instance"));
46769
+ return json(cursor);
46770
+ } catch (e) {
46771
+ return err(e);
46772
+ }
46773
+ });
46774
+ server.tool("browser_tui_assert", `Assert conditions on the terminal state. Chain multiple conditions with AND.
46775
+
46776
+ CONDITION SYNTAX:
46777
+ - "text contains X" \u2014 terminal buffer contains substring X
46778
+ - "row N contains X" \u2014 row N (0-indexed) contains substring X
46779
+ - "cursor at R,C" \u2014 cursor is at row R, column C
46780
+ - "row_count > N" \u2014 total rows greater than N
46781
+ - "row_count == N" \u2014 total rows equals N
46782
+
46783
+ Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
46784
+ session_id: exports_external.string().optional(),
46785
+ condition: exports_external.string().describe("Assertion condition(s), joined with AND")
46786
+ }, async ({ session_id, condition }) => {
46787
+ try {
46788
+ const sid = resolveSessionId(session_id);
46789
+ assertTuiSession(sid);
46790
+ const page = getSessionPage(sid);
46791
+ const termData = await getTermText(page);
46792
+ const cursor = await page.evaluate(() => {
46793
+ const term = window.term ?? window.terminal;
46794
+ if (!term?.buffer?.active)
46795
+ return { row: -1, col: -1 };
46796
+ return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
46797
+ });
46798
+ const checks = [];
46799
+ let allPassed = true;
46800
+ for (const part of condition.split(/\s+AND\s+/i)) {
46801
+ const trimmed = part.trim();
46802
+ let result = false;
46803
+ if (/^text\s+contains\s+/i.test(trimmed)) {
46804
+ const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
46805
+ result = termData.text.includes(needle);
46806
+ } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
46807
+ const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
46808
+ if (match) {
46809
+ const rowIdx = parseInt(match[1]);
46810
+ const needle = match[2].replace(/^["']|["']$/g, "");
46811
+ result = (termData.rows[rowIdx] ?? "").includes(needle);
46812
+ }
46813
+ } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
46814
+ const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
46815
+ if (match) {
46816
+ result = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
46817
+ }
46818
+ } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
46819
+ const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
46820
+ if (match) {
46821
+ const op = match[1];
46822
+ const n = parseInt(match[2]);
46823
+ const count = termData.row_count;
46824
+ result = op === ">" ? count > n : op === ">=" ? count >= n : op === "<" ? count < n : op === "<=" ? count <= n : op === "==" ? count === n : count !== n;
46825
+ }
46826
+ }
46827
+ checks.push({ assertion: trimmed, result });
46828
+ if (!result)
46829
+ allPassed = false;
46830
+ }
46831
+ return json({ passed: allPassed, checks, cursor, row_count: termData.row_count });
46832
+ } catch (e) {
46833
+ return err(e);
46834
+ }
46835
+ });
46836
+ 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.", {
46837
+ session_id: exports_external.string().optional()
46838
+ }, async ({ session_id }) => {
46839
+ try {
46840
+ const sid = resolveSessionId(session_id);
46841
+ assertTuiSession(sid);
46842
+ const page = getSessionPage(sid);
46843
+ const snapshot = await page.evaluate(() => {
46844
+ const term = window.term ?? window.terminal;
46845
+ if (!term?.buffer?.active)
46846
+ return null;
46847
+ const buf = term.buffer.active;
46848
+ const rows = [];
46849
+ for (let i = 0;i < buf.length; i++) {
46850
+ const line = buf.getLine(i);
46851
+ if (line)
46852
+ rows.push(line.translateToString(true));
46853
+ }
46854
+ return {
46855
+ rows,
46856
+ cols: term.cols,
46857
+ total_rows: term.rows,
46858
+ buffer_length: buf.length,
46859
+ cursor_row: buf.cursorY,
46860
+ cursor_col: buf.cursorX,
46861
+ font_size: term.options?.fontSize,
46862
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
46863
+ };
46864
+ });
46865
+ if (!snapshot)
46866
+ return err(new Error("Could not capture snapshot \u2014 no terminal instance"));
46867
+ return json(snapshot);
46868
+ } catch (e) {
46869
+ return err(e);
46870
+ }
46871
+ });
46872
+ 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.", {
46873
+ session_id: exports_external.string().optional(),
46874
+ interval_ms: exports_external.number().optional().default(500).describe("Polling interval in ms (default: 500)")
46875
+ }, async ({ session_id, interval_ms }) => {
46876
+ try {
46877
+ const sid = resolveSessionId(session_id);
46878
+ assertTuiSession(sid);
46879
+ const page = getSessionPage(sid);
46880
+ if (activeRecordings2.has(sid)) {
46881
+ return err(new Error("Recording already active for this session. Stop it first with browser_tui_record_stop."));
46882
+ }
46883
+ const dims = await page.evaluate(() => {
46884
+ const term = window.term ?? window.terminal;
46885
+ return term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
46886
+ });
46887
+ const initialText = (await getTermText(page)).text;
46888
+ const recording = {
46889
+ sessionId: sid,
46890
+ startTime: Date.now(),
46891
+ cols: dims.cols,
46892
+ rows: dims.rows,
46893
+ events: [],
46894
+ lastText: initialText,
46895
+ intervalId: setInterval(async () => {
46896
+ try {
46897
+ const current = await getTermText(page);
46898
+ if (current.text !== recording.lastText) {
46899
+ const elapsed = (Date.now() - recording.startTime) / 1000;
46900
+ recording.events.push([elapsed, "o", current.text.slice(recording.lastText.length) || current.text]);
46901
+ recording.lastText = current.text;
46902
+ }
46903
+ } catch {}
46904
+ }, interval_ms)
46905
+ };
46906
+ activeRecordings2.set(sid, recording);
46907
+ return json({ recording: true, session_id: sid, interval_ms, cols: dims.cols, rows: dims.rows });
46908
+ } catch (e) {
46909
+ return err(e);
46910
+ }
46911
+ });
46912
+ server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON. Compatible with asciinema player.", {
46913
+ session_id: exports_external.string().optional()
46914
+ }, async ({ session_id }) => {
46915
+ try {
46916
+ const sid = resolveSessionId(session_id);
46917
+ const recording = activeRecordings2.get(sid);
46918
+ if (!recording)
46919
+ return err(new Error("No active recording for this session"));
46920
+ clearInterval(recording.intervalId);
46921
+ activeRecordings2.delete(sid);
46922
+ const duration = (Date.now() - recording.startTime) / 1000;
46923
+ const header = {
46924
+ version: 2,
46925
+ width: recording.cols,
46926
+ height: recording.rows,
46927
+ timestamp: Math.floor(recording.startTime / 1000),
46928
+ duration,
46929
+ env: { TERM: "xterm-256color", SHELL: "/bin/bash" }
46930
+ };
46931
+ const lines = [JSON.stringify(header)];
46932
+ for (const [time, type2, data] of recording.events) {
46933
+ lines.push(JSON.stringify([time, type2, data]));
46934
+ }
46935
+ const asciicast = lines.join(`
46936
+ `);
46937
+ return json({
46938
+ format: "asciicast_v2",
46939
+ duration_seconds: Math.round(duration * 10) / 10,
46940
+ event_count: recording.events.length,
46941
+ asciicast
46942
+ });
46943
+ } catch (e) {
46944
+ return err(e);
46945
+ }
46946
+ });
46947
+ }
46948
+
46434
46949
  // src/mcp/index.ts
46435
46950
  init_schema();
46436
46951
  var _pkg = JSON.parse(readFileSync11(join20(import.meta.dir, "../../package.json"), "utf8"));
@@ -46443,6 +46958,7 @@ register2(server);
46443
46958
  register3(server);
46444
46959
  register4(server);
46445
46960
  register8(server);
46961
+ register9(server);
46446
46962
  server.tool("send_feedback", "Send feedback about this service", { message: exports_external.string(), email: exports_external.string().optional(), category: exports_external.enum(["bug", "feature", "general"]).optional() }, async (params) => {
46447
46963
  try {
46448
46964
  const db2 = getDatabase();
@@ -1 +1 @@
1
- {"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/mcp/meta.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA8CzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QAq7BzC"}
1
+ {"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/mcp/meta.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA8CzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QAi8BzC"}
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/mcp/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgCzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QAoXzC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../../src/mcp/sessions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgCzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QA2YzC"}
@@ -0,0 +1,3 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
3
+ //# sourceMappingURL=tui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/mcp/tui.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4FzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,QA2ZzC"}