@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/cli/index.js CHANGED
@@ -12691,7 +12691,42 @@ async function launchTui(command, options = {}) {
12691
12691
  waitUntil: "domcontentloaded"
12692
12692
  });
12693
12693
  await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
12694
- return { ttydProcess, port, browser, page };
12694
+ let resolvedTheme = "dark";
12695
+ const requestedTheme = options.theme ?? "system";
12696
+ if (requestedTheme === "light") {
12697
+ resolvedTheme = "light";
12698
+ } else if (requestedTheme === "dark") {
12699
+ resolvedTheme = "dark";
12700
+ } else {
12701
+ try {
12702
+ const result = execSync2("defaults read -g AppleInterfaceStyle 2>/dev/null", { encoding: "utf8" }).trim();
12703
+ resolvedTheme = result === "Dark" ? "dark" : "light";
12704
+ } catch {
12705
+ resolvedTheme = "light";
12706
+ }
12707
+ }
12708
+ const themeColors = THEMES[resolvedTheme];
12709
+ await page.evaluate((theme) => {
12710
+ const term = window.term ?? window.terminal;
12711
+ if (term?.options) {
12712
+ term.options.theme = theme;
12713
+ }
12714
+ document.body.style.backgroundColor = theme.background;
12715
+ const container = document.getElementById("terminal-container");
12716
+ if (container)
12717
+ container.style.backgroundColor = theme.background;
12718
+ const viewport2 = document.querySelector(".xterm-viewport");
12719
+ if (viewport2)
12720
+ viewport2.style.backgroundColor = theme.background;
12721
+ }, themeColors);
12722
+ if (options.fontSize) {
12723
+ await page.evaluate((size) => {
12724
+ const term = window.term ?? window.terminal;
12725
+ if (term?.options)
12726
+ term.options.fontSize = size;
12727
+ }, options.fontSize);
12728
+ }
12729
+ return { ttydProcess, port, browser, page, theme: resolvedTheme };
12695
12730
  } catch (err) {
12696
12731
  ttydProcess.kill();
12697
12732
  throw err;
@@ -12708,11 +12743,57 @@ async function closeTui(session) {
12708
12743
  session.ttydProcess.kill("SIGTERM");
12709
12744
  } catch {}
12710
12745
  }
12711
- var DEFAULT_TTYD_PORT_START = 7780, nextPort;
12746
+ var DEFAULT_TTYD_PORT_START = 7780, nextPort, THEMES;
12712
12747
  var init_tui = __esm(() => {
12713
12748
  init_types();
12714
12749
  init_playwright();
12715
12750
  nextPort = DEFAULT_TTYD_PORT_START;
12751
+ THEMES = {
12752
+ dark: {
12753
+ background: "#1e1e1e",
12754
+ foreground: "#d4d4d4",
12755
+ cursor: "#d4d4d4",
12756
+ selectionBackground: "#264f78",
12757
+ black: "#1e1e1e",
12758
+ red: "#f44747",
12759
+ green: "#6a9955",
12760
+ yellow: "#d7ba7d",
12761
+ blue: "#569cd6",
12762
+ magenta: "#c586c0",
12763
+ cyan: "#4ec9b0",
12764
+ white: "#d4d4d4",
12765
+ brightBlack: "#808080",
12766
+ brightRed: "#f44747",
12767
+ brightGreen: "#6a9955",
12768
+ brightYellow: "#d7ba7d",
12769
+ brightBlue: "#569cd6",
12770
+ brightMagenta: "#c586c0",
12771
+ brightCyan: "#4ec9b0",
12772
+ brightWhite: "#ffffff"
12773
+ },
12774
+ light: {
12775
+ background: "#ffffff",
12776
+ foreground: "#1e1e1e",
12777
+ cursor: "#1e1e1e",
12778
+ selectionBackground: "#add6ff",
12779
+ black: "#1e1e1e",
12780
+ red: "#cd3131",
12781
+ green: "#008000",
12782
+ yellow: "#795e26",
12783
+ blue: "#0451a5",
12784
+ magenta: "#af00db",
12785
+ cyan: "#0598bc",
12786
+ white: "#d4d4d4",
12787
+ brightBlack: "#808080",
12788
+ brightRed: "#cd3131",
12789
+ brightGreen: "#008000",
12790
+ brightYellow: "#795e26",
12791
+ brightBlue: "#0451a5",
12792
+ brightMagenta: "#af00db",
12793
+ brightCyan: "#0598bc",
12794
+ brightWhite: "#ffffff"
12795
+ }
12796
+ };
12716
12797
  });
12717
12798
 
12718
12799
  // src/engines/selector.ts
@@ -14149,7 +14230,9 @@ async function createSession2(opts = {}) {
14149
14230
  const command = opts.startUrl ?? "bash";
14150
14231
  const tuiSess = await launchTui(command, {
14151
14232
  headless: opts.headless ?? true,
14152
- viewport: opts.viewport
14233
+ viewport: opts.viewport,
14234
+ theme: opts.tuiTheme ?? "system",
14235
+ fontSize: opts.tuiFontSize
14153
14236
  });
14154
14237
  browser = tuiSess.browser;
14155
14238
  page = tuiSess.page;
@@ -27577,12 +27660,26 @@ var init_helpers = __esm(() => {
27577
27660
 
27578
27661
  // src/mcp/sessions.ts
27579
27662
  function register4(server) {
27580
- 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.", {
27581
- engine: exports_external2.enum(["playwright", "cdp", "lightpanda", "bun", "tui", "auto"]).optional().default("auto"),
27582
- use_case: exports_external2.string().optional(),
27663
+ server.tool("browser_session_create", `Create a new browser session. Returns a session object with an id you must pass to other tools.
27664
+
27665
+ ENGINES:
27666
+ - "auto" (default): picks the best engine for your use case automatically
27667
+ - "playwright": full browser automation \u2014 forms, SPAs, auth flows, multi-tab
27668
+ - "cdp": Chrome DevTools Protocol \u2014 network monitoring, perf profiling, script injection
27669
+ - "lightpanda": fast headless for static pages
27670
+ - "bun": native Bun.WebView \u2014 fastest for screenshots and scraping
27671
+ - "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.
27672
+
27673
+ TIPS:
27674
+ - If agent_id is set and already has an active session, returns the existing one (use force_new to override)
27675
+ - If session_id is omitted on other tools, the single active session is auto-selected
27676
+ - Use cdp_url to attach to an already-running Chrome instance
27677
+ - For TUI sessions: start_url is the shell command to run, NOT a URL`, {
27678
+ engine: exports_external2.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"),
27679
+ use_case: exports_external2.string().optional().describe("Hint for auto engine selection: scrape, screenshot, form, auth, network, har, perf, terminal, tui"),
27583
27680
  project_id: exports_external2.string().optional(),
27584
27681
  agent_id: exports_external2.string().optional(),
27585
- start_url: exports_external2.string().optional(),
27682
+ start_url: exports_external2.string().optional().describe("URL to navigate to, OR for engine='tui': the shell command to run (e.g. 'htop', 'bun run app.tsx')"),
27586
27683
  headless: exports_external2.boolean().optional().default(true),
27587
27684
  viewport_width: exports_external2.number().optional().default(1280),
27588
27685
  viewport_height: exports_external2.number().optional().default(720),
@@ -27591,8 +27688,10 @@ function register4(server) {
27591
27688
  storage_state: exports_external2.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
27592
27689
  force_new: exports_external2.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
27593
27690
  tags: exports_external2.array(exports_external2.string()).optional(),
27594
- 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")
27595
- }, 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 }) => {
27691
+ 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"),
27692
+ 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."),
27693
+ 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.")
27694
+ }, 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 }) => {
27596
27695
  try {
27597
27696
  if (agent_id && !force_new) {
27598
27697
  const existing = getActiveSessionForAgent2(agent_id);
@@ -27610,7 +27709,9 @@ function register4(server) {
27610
27709
  stealth,
27611
27710
  autoGallery: auto_gallery,
27612
27711
  storageState: storage_state,
27613
- cdpUrl: cdp_url
27712
+ cdpUrl: cdp_url,
27713
+ tuiTheme: tui_theme,
27714
+ tuiFontSize: tui_font_size
27614
27715
  });
27615
27716
  if (tags?.length) {
27616
27717
  const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
@@ -48176,6 +48277,18 @@ function register10(server) {
48176
48277
  { tool: "browser_tab_switch", description: "Switch to a tab by index" },
48177
48278
  { tool: "browser_tab_close", description: "Close a tab by index" }
48178
48279
  ],
48280
+ TUI: [
48281
+ { tool: "browser_tui_send_keys", description: "Send keystrokes (ctrl+c, arrow_up, tab, enter, etc.)" },
48282
+ { tool: "browser_tui_send_text", description: "Type text + optional Enter (most common TUI interaction)" },
48283
+ { tool: "browser_tui_resize", description: "Resize terminal cols/rows mid-session" },
48284
+ { tool: "browser_tui_get_text", description: "Get terminal text buffer (full or row range)" },
48285
+ { tool: "browser_tui_wait_for_text", description: "Wait for text to appear in terminal output" },
48286
+ { tool: "browser_tui_get_cursor", description: "Get cursor position (row, col)" },
48287
+ { tool: "browser_tui_assert", description: "Assert terminal conditions (text contains, row N contains, cursor at)" },
48288
+ { tool: "browser_tui_snapshot", description: "Structured terminal snapshot (rows array, cursor, dimensions)" },
48289
+ { tool: "browser_tui_record_start", description: "Start recording terminal as asciicast" },
48290
+ { tool: "browser_tui_record_stop", description: "Stop recording, return asciicast v2 JSON" }
48291
+ ],
48179
48292
  Meta: [
48180
48293
  { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
48181
48294
  { tool: "browser_version", description: "Show running binary version and tool count" },
@@ -48588,6 +48701,412 @@ var init_data = __esm(() => {
48588
48701
  init_meta();
48589
48702
  });
48590
48703
 
48704
+ // src/mcp/tui.ts
48705
+ function assertTuiSession(sessionId) {
48706
+ const { getSessionEngine: getSessionEngine2 } = (init_session(), __toCommonJS(exports_session));
48707
+ const engine = getSessionEngine2(sessionId);
48708
+ if (engine !== "tui") {
48709
+ 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")`);
48710
+ }
48711
+ }
48712
+ async function getTermText(page, startRow, endRow) {
48713
+ const result = await page.evaluate((args) => {
48714
+ const [sr, er] = args;
48715
+ const term = window.term ?? window.terminal;
48716
+ if (!term?.buffer?.active)
48717
+ return { text: "", rows: [], row_count: 0 };
48718
+ const buf = term.buffer.active;
48719
+ const allRows = [];
48720
+ for (let i = 0;i < buf.length; i++) {
48721
+ const line = buf.getLine(i);
48722
+ if (line)
48723
+ allRows.push(line.translateToString(true));
48724
+ }
48725
+ const start = sr ?? 0;
48726
+ const end = er ?? allRows.length;
48727
+ const filtered = allRows.slice(start, end);
48728
+ return { text: filtered.join(`
48729
+ `).trimEnd(), rows: filtered, row_count: allRows.length };
48730
+ }, [startRow, endRow]);
48731
+ return result;
48732
+ }
48733
+ function register12(server) {
48734
+ server.tool("browser_tui_send_keys", `Send keystrokes to a TUI terminal session. Use friendly key names.
48735
+
48736
+ SUPPORTED KEYS:
48737
+ - Control: ctrl+c, ctrl+d, ctrl+z, ctrl+l, ctrl+a, ctrl+e, ctrl+k, ctrl+u, ctrl+w, ctrl+r
48738
+ - Navigation: enter, tab, escape, backspace, delete, space
48739
+ - Arrows: up, down, left, right (or arrow_up, arrow_down, arrow_left, arrow_right)
48740
+ - Function: f1-f12
48741
+ - Position: home, end, page_up, page_down
48742
+
48743
+ Pass multiple keys as a comma-separated string: "tab,tab,enter" or "ctrl+c"
48744
+ For typing text, use browser_tui_send_text instead.`, {
48745
+ session_id: exports_external2.string().optional(),
48746
+ keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'")
48747
+ }, async ({ session_id, keys }) => {
48748
+ try {
48749
+ const sid = resolveSessionId(session_id);
48750
+ assertTuiSession(sid);
48751
+ const page = getSessionPage(sid);
48752
+ const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
48753
+ const sent = [];
48754
+ for (const key of keyList) {
48755
+ const mapped = KEY_MAP[key];
48756
+ if (mapped) {
48757
+ if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
48758
+ await page.keyboard.insertText(mapped);
48759
+ } else {
48760
+ await page.keyboard.press(mapped);
48761
+ }
48762
+ sent.push(key);
48763
+ } else {
48764
+ await page.keyboard.press(key);
48765
+ sent.push(key);
48766
+ }
48767
+ }
48768
+ return json({ sent, count: sent.length });
48769
+ } catch (e) {
48770
+ return err(e);
48771
+ }
48772
+ });
48773
+ 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.
48774
+
48775
+ Examples:
48776
+ - Send a command: text="ls -la", press_enter=true
48777
+ - Type without executing: text="partial input", press_enter=false
48778
+ - Send to a prompt: text="yes", press_enter=true`, {
48779
+ session_id: exports_external2.string().optional(),
48780
+ text: exports_external2.string().describe("Text to type into the terminal"),
48781
+ press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)")
48782
+ }, async ({ session_id, text, press_enter }) => {
48783
+ try {
48784
+ const sid = resolveSessionId(session_id);
48785
+ assertTuiSession(sid);
48786
+ const page = getSessionPage(sid);
48787
+ const textarea = await page.$(".xterm-helper-textarea");
48788
+ if (textarea) {
48789
+ await textarea.type(text);
48790
+ } else {
48791
+ await page.keyboard.type(text);
48792
+ }
48793
+ if (press_enter) {
48794
+ await page.keyboard.press("Enter");
48795
+ }
48796
+ return json({ typed: text, pressed_enter: press_enter });
48797
+ } catch (e) {
48798
+ return err(e);
48799
+ }
48800
+ });
48801
+ 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.", {
48802
+ session_id: exports_external2.string().optional(),
48803
+ cols: exports_external2.number().describe("Number of columns (e.g. 80, 120, 200)"),
48804
+ rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)")
48805
+ }, async ({ session_id, cols, rows }) => {
48806
+ try {
48807
+ const sid = resolveSessionId(session_id);
48808
+ assertTuiSession(sid);
48809
+ const page = getSessionPage(sid);
48810
+ const result = await page.evaluate((args) => {
48811
+ const [c, r] = args;
48812
+ const term = window.term ?? window.terminal;
48813
+ if (!term)
48814
+ return { resized: false, error: "No terminal instance found" };
48815
+ term.resize(c, r);
48816
+ return { resized: true, cols: c, rows: r };
48817
+ }, [cols, rows]);
48818
+ return json(result);
48819
+ } catch (e) {
48820
+ return err(e);
48821
+ }
48822
+ });
48823
+ server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.
48824
+
48825
+ Use this to read what the terminal is currently displaying. For waiting until specific text appears, use browser_tui_wait_for_text instead.`, {
48826
+ session_id: exports_external2.string().optional(),
48827
+ start_row: exports_external2.number().optional().describe("First row to read (0-indexed, default: 0)"),
48828
+ end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows.")
48829
+ }, async ({ session_id, start_row, end_row }) => {
48830
+ try {
48831
+ const sid = resolveSessionId(session_id);
48832
+ assertTuiSession(sid);
48833
+ const page = getSessionPage(sid);
48834
+ const result = await getTermText(page, start_row, end_row);
48835
+ return json(result);
48836
+ } catch (e) {
48837
+ return err(e);
48838
+ }
48839
+ });
48840
+ 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.
48841
+
48842
+ Use this after sending a command to wait for its output, or to wait for a TUI app to finish loading.`, {
48843
+ session_id: exports_external2.string().optional(),
48844
+ text: exports_external2.string().describe("Text to wait for (substring match)"),
48845
+ timeout_ms: exports_external2.number().optional().default(30000).describe("Timeout in milliseconds (default: 30000)")
48846
+ }, async ({ session_id, text, timeout_ms }) => {
48847
+ try {
48848
+ const sid = resolveSessionId(session_id);
48849
+ assertTuiSession(sid);
48850
+ const page = getSessionPage(sid);
48851
+ const start = Date.now();
48852
+ while (Date.now() - start < timeout_ms) {
48853
+ const result = await getTermText(page);
48854
+ if (result.text.includes(text)) {
48855
+ return json({ found: true, elapsed_ms: Date.now() - start, terminal_text: result.text });
48856
+ }
48857
+ await new Promise((r) => setTimeout(r, 250));
48858
+ }
48859
+ const finalText = await getTermText(page);
48860
+ return json({ found: false, elapsed_ms: timeout_ms, terminal_text: finalText.text });
48861
+ } catch (e) {
48862
+ return err(e);
48863
+ }
48864
+ });
48865
+ server.tool("browser_tui_get_cursor", "Get the current cursor position (row and column) in the terminal.", {
48866
+ session_id: exports_external2.string().optional()
48867
+ }, async ({ session_id }) => {
48868
+ try {
48869
+ const sid = resolveSessionId(session_id);
48870
+ assertTuiSession(sid);
48871
+ const page = getSessionPage(sid);
48872
+ const cursor = await page.evaluate(() => {
48873
+ const term = window.term ?? window.terminal;
48874
+ if (!term?.buffer?.active)
48875
+ return null;
48876
+ return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
48877
+ });
48878
+ if (!cursor)
48879
+ return err(new Error("Could not read cursor position \u2014 no terminal instance"));
48880
+ return json(cursor);
48881
+ } catch (e) {
48882
+ return err(e);
48883
+ }
48884
+ });
48885
+ server.tool("browser_tui_assert", `Assert conditions on the terminal state. Chain multiple conditions with AND.
48886
+
48887
+ CONDITION SYNTAX:
48888
+ - "text contains X" \u2014 terminal buffer contains substring X
48889
+ - "row N contains X" \u2014 row N (0-indexed) contains substring X
48890
+ - "cursor at R,C" \u2014 cursor is at row R, column C
48891
+ - "row_count > N" \u2014 total rows greater than N
48892
+ - "row_count == N" \u2014 total rows equals N
48893
+
48894
+ Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
48895
+ session_id: exports_external2.string().optional(),
48896
+ condition: exports_external2.string().describe("Assertion condition(s), joined with AND")
48897
+ }, async ({ session_id, condition }) => {
48898
+ try {
48899
+ const sid = resolveSessionId(session_id);
48900
+ assertTuiSession(sid);
48901
+ const page = getSessionPage(sid);
48902
+ const termData = await getTermText(page);
48903
+ const cursor = await page.evaluate(() => {
48904
+ const term = window.term ?? window.terminal;
48905
+ if (!term?.buffer?.active)
48906
+ return { row: -1, col: -1 };
48907
+ return { row: term.buffer.active.cursorY, col: term.buffer.active.cursorX };
48908
+ });
48909
+ const checks = [];
48910
+ let allPassed = true;
48911
+ for (const part of condition.split(/\s+AND\s+/i)) {
48912
+ const trimmed = part.trim();
48913
+ let result = false;
48914
+ if (/^text\s+contains\s+/i.test(trimmed)) {
48915
+ const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
48916
+ result = termData.text.includes(needle);
48917
+ } else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
48918
+ const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
48919
+ if (match) {
48920
+ const rowIdx = parseInt(match[1]);
48921
+ const needle = match[2].replace(/^["']|["']$/g, "");
48922
+ result = (termData.rows[rowIdx] ?? "").includes(needle);
48923
+ }
48924
+ } else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
48925
+ const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
48926
+ if (match) {
48927
+ result = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
48928
+ }
48929
+ } else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
48930
+ const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
48931
+ if (match) {
48932
+ const op = match[1];
48933
+ const n = parseInt(match[2]);
48934
+ const count = termData.row_count;
48935
+ result = op === ">" ? count > n : op === ">=" ? count >= n : op === "<" ? count < n : op === "<=" ? count <= n : op === "==" ? count === n : count !== n;
48936
+ }
48937
+ }
48938
+ checks.push({ assertion: trimmed, result });
48939
+ if (!result)
48940
+ allPassed = false;
48941
+ }
48942
+ return json({ passed: allPassed, checks, cursor, row_count: termData.row_count });
48943
+ } catch (e) {
48944
+ return err(e);
48945
+ }
48946
+ });
48947
+ 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.", {
48948
+ session_id: exports_external2.string().optional()
48949
+ }, async ({ session_id }) => {
48950
+ try {
48951
+ const sid = resolveSessionId(session_id);
48952
+ assertTuiSession(sid);
48953
+ const page = getSessionPage(sid);
48954
+ const snapshot = await page.evaluate(() => {
48955
+ const term = window.term ?? window.terminal;
48956
+ if (!term?.buffer?.active)
48957
+ return null;
48958
+ const buf = term.buffer.active;
48959
+ const rows = [];
48960
+ for (let i = 0;i < buf.length; i++) {
48961
+ const line = buf.getLine(i);
48962
+ if (line)
48963
+ rows.push(line.translateToString(true));
48964
+ }
48965
+ return {
48966
+ rows,
48967
+ cols: term.cols,
48968
+ total_rows: term.rows,
48969
+ buffer_length: buf.length,
48970
+ cursor_row: buf.cursorY,
48971
+ cursor_col: buf.cursorX,
48972
+ font_size: term.options?.fontSize,
48973
+ theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
48974
+ };
48975
+ });
48976
+ if (!snapshot)
48977
+ return err(new Error("Could not capture snapshot \u2014 no terminal instance"));
48978
+ return json(snapshot);
48979
+ } catch (e) {
48980
+ return err(e);
48981
+ }
48982
+ });
48983
+ 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.", {
48984
+ session_id: exports_external2.string().optional(),
48985
+ interval_ms: exports_external2.number().optional().default(500).describe("Polling interval in ms (default: 500)")
48986
+ }, async ({ session_id, interval_ms }) => {
48987
+ try {
48988
+ const sid = resolveSessionId(session_id);
48989
+ assertTuiSession(sid);
48990
+ const page = getSessionPage(sid);
48991
+ if (activeRecordings2.has(sid)) {
48992
+ return err(new Error("Recording already active for this session. Stop it first with browser_tui_record_stop."));
48993
+ }
48994
+ const dims = await page.evaluate(() => {
48995
+ const term = window.term ?? window.terminal;
48996
+ return term ? { cols: term.cols, rows: term.rows } : { cols: 80, rows: 24 };
48997
+ });
48998
+ const initialText = (await getTermText(page)).text;
48999
+ const recording = {
49000
+ sessionId: sid,
49001
+ startTime: Date.now(),
49002
+ cols: dims.cols,
49003
+ rows: dims.rows,
49004
+ events: [],
49005
+ lastText: initialText,
49006
+ intervalId: setInterval(async () => {
49007
+ try {
49008
+ const current = await getTermText(page);
49009
+ if (current.text !== recording.lastText) {
49010
+ const elapsed = (Date.now() - recording.startTime) / 1000;
49011
+ recording.events.push([elapsed, "o", current.text.slice(recording.lastText.length) || current.text]);
49012
+ recording.lastText = current.text;
49013
+ }
49014
+ } catch {}
49015
+ }, interval_ms)
49016
+ };
49017
+ activeRecordings2.set(sid, recording);
49018
+ return json({ recording: true, session_id: sid, interval_ms, cols: dims.cols, rows: dims.rows });
49019
+ } catch (e) {
49020
+ return err(e);
49021
+ }
49022
+ });
49023
+ server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON. Compatible with asciinema player.", {
49024
+ session_id: exports_external2.string().optional()
49025
+ }, async ({ session_id }) => {
49026
+ try {
49027
+ const sid = resolveSessionId(session_id);
49028
+ const recording = activeRecordings2.get(sid);
49029
+ if (!recording)
49030
+ return err(new Error("No active recording for this session"));
49031
+ clearInterval(recording.intervalId);
49032
+ activeRecordings2.delete(sid);
49033
+ const duration = (Date.now() - recording.startTime) / 1000;
49034
+ const header = {
49035
+ version: 2,
49036
+ width: recording.cols,
49037
+ height: recording.rows,
49038
+ timestamp: Math.floor(recording.startTime / 1000),
49039
+ duration,
49040
+ env: { TERM: "xterm-256color", SHELL: "/bin/bash" }
49041
+ };
49042
+ const lines = [JSON.stringify(header)];
49043
+ for (const [time, type2, data] of recording.events) {
49044
+ lines.push(JSON.stringify([time, type2, data]));
49045
+ }
49046
+ const asciicast = lines.join(`
49047
+ `);
49048
+ return json({
49049
+ format: "asciicast_v2",
49050
+ duration_seconds: Math.round(duration * 10) / 10,
49051
+ event_count: recording.events.length,
49052
+ asciicast
49053
+ });
49054
+ } catch (e) {
49055
+ return err(e);
49056
+ }
49057
+ });
49058
+ }
49059
+ var KEY_MAP, activeRecordings2;
49060
+ var init_tui2 = __esm(() => {
49061
+ init_helpers();
49062
+ KEY_MAP = {
49063
+ "ctrl+c": "\x03",
49064
+ "ctrl+d": "\x04",
49065
+ "ctrl+z": "\x1A",
49066
+ "ctrl+l": "\f",
49067
+ "ctrl+a": "\x01",
49068
+ "ctrl+e": "\x05",
49069
+ "ctrl+k": "\v",
49070
+ "ctrl+u": "\x15",
49071
+ "ctrl+w": "\x17",
49072
+ "ctrl+r": "\x12",
49073
+ "ctrl+p": "\x10",
49074
+ "ctrl+n": "\x0E",
49075
+ enter: "Enter",
49076
+ tab: "Tab",
49077
+ escape: "Escape",
49078
+ esc: "Escape",
49079
+ backspace: "Backspace",
49080
+ delete: "Delete",
49081
+ space: " ",
49082
+ up: "ArrowUp",
49083
+ down: "ArrowDown",
49084
+ left: "ArrowLeft",
49085
+ right: "ArrowRight",
49086
+ arrow_up: "ArrowUp",
49087
+ arrow_down: "ArrowDown",
49088
+ arrow_left: "ArrowLeft",
49089
+ arrow_right: "ArrowRight",
49090
+ home: "Home",
49091
+ end: "End",
49092
+ page_up: "PageUp",
49093
+ page_down: "PageDown",
49094
+ f1: "F1",
49095
+ f2: "F2",
49096
+ f3: "F3",
49097
+ f4: "F4",
49098
+ f5: "F5",
49099
+ f6: "F6",
49100
+ f7: "F7",
49101
+ f8: "F8",
49102
+ f9: "F9",
49103
+ f10: "F10",
49104
+ f11: "F11",
49105
+ f12: "F12"
49106
+ };
49107
+ activeRecordings2 = new Map;
49108
+ });
49109
+
48591
49110
  // src/mcp/index.ts
48592
49111
  var exports_mcp = {};
48593
49112
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -48601,6 +49120,7 @@ var init_mcp = __esm(async () => {
48601
49120
  init_capture();
48602
49121
  init_network2();
48603
49122
  init_data();
49123
+ init_tui2();
48604
49124
  init_zod2();
48605
49125
  init_schema();
48606
49126
  _pkg = JSON.parse(readFileSync10(join21(import.meta.dir, "../../package.json"), "utf8"));
@@ -48613,6 +49133,7 @@ var init_mcp = __esm(async () => {
48613
49133
  register6(server);
48614
49134
  register7(server);
48615
49135
  register11(server);
49136
+ register12(server);
48616
49137
  server.tool("send_feedback", "Send feedback about this service", { message: exports_external2.string(), email: exports_external2.string().optional(), category: exports_external2.enum(["bug", "feature", "general"]).optional() }, async (params) => {
48617
49138
  try {
48618
49139
  const db2 = getDatabase();
@@ -49442,7 +49963,7 @@ init_recorder();
49442
49963
  init_recordings();
49443
49964
  init_lightpanda();
49444
49965
  import chalk4 from "chalk";
49445
- function register12(program2) {
49966
+ function register13(program2) {
49446
49967
  const recordCmd = program2.command("record").description("Manage action recordings");
49447
49968
  recordCmd.command("start <name>").description("Start recording actions in a new session").option("--url <url>", "Start URL").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").action(async (name, opts) => {
49448
49969
  const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
@@ -49847,5 +50368,5 @@ program2.name("browser").description("@hasna/browser \u2014 general-purpose brow
49847
50368
  register(program2);
49848
50369
  register2(program2);
49849
50370
  register3(program2);
49850
- register12(program2);
50371
+ register13(program2);
49851
50372
  program2.parseAsync(process.argv);
@@ -1,10 +1,12 @@
1
1
  import { type ChildProcess } from "node:child_process";
2
2
  import type { Browser, Page } from "playwright";
3
+ export type TuiTheme = "dark" | "light" | "system";
3
4
  export interface TuiSession {
4
5
  ttydProcess: ChildProcess;
5
6
  port: number;
6
7
  browser: Browser;
7
8
  page: Page;
9
+ theme: TuiTheme;
8
10
  }
9
11
  /**
10
12
  * Check if ttyd is installed on this system.
@@ -23,6 +25,8 @@ export declare function launchTui(command: string, options?: {
23
25
  width: number;
24
26
  height: number;
25
27
  };
28
+ theme?: TuiTheme;
29
+ fontSize?: number;
26
30
  }): Promise<TuiSession>;
27
31
  /**
28
32
  * Send keystrokes to the TUI app via the Playwright page.
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/engines/tui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAYhD,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,YAAY,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAOxC;AAqCD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC,GACL,OAAO,CAAC,UAAU,CAAC,CAqDrB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAStE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBjE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAe,GACzB,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjE"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../../src/engines/tui.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAYhD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEnD,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,YAAY,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,IAAI,CAAC;IACX,KAAK,EAAE,QAAQ,CAAC;CACjB;AAkDD;;GAEG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAOxC;AAqCD;;;;;;GAMG;AACH,wBAAsB,SAAS,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7C,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACd,GACL,OAAO,CAAC,UAAU,CAAC,CA+FrB;AAED;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAStE;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO3E;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBjE;AAED;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,MAAM,EACZ,SAAS,GAAE,MAAe,GACzB,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;GAEG;AACH,wBAAsB,QAAQ,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAUjE"}