@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/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
|
-
|
|
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",
|
|
43819
|
-
|
|
43820
|
-
|
|
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
|
-
|
|
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();
|
package/dist/mcp/meta.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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 @@
|
|
|
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"}
|