@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 +533 -12
- package/dist/engines/tui.d.ts +4 -0
- package/dist/engines/tui.d.ts.map +1 -1
- package/dist/index.js +85 -2
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/mcp/index.js +526 -10
- package/dist/mcp/meta.d.ts.map +1 -1
- package/dist/mcp/sessions.d.ts.map +1 -1
- package/dist/mcp/tui.d.ts +3 -0
- package/dist/mcp/tui.d.ts.map +1 -0
- package/dist/server/index.js +85 -2
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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",
|
|
27581
|
-
|
|
27582
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
50371
|
+
register13(program2);
|
|
49851
50372
|
program2.parseAsync(process.argv);
|
package/dist/engines/tui.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|