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