@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/cli/index.js
CHANGED
|
@@ -14372,6 +14372,340 @@ var init_bun_webview = __esm(() => {
|
|
|
14372
14372
|
|
|
14373
14373
|
// src/engines/tui.ts
|
|
14374
14374
|
import { execSync as execSync2, spawn as spawn2 } from "child_process";
|
|
14375
|
+
function normalizeRowText(text) {
|
|
14376
|
+
return text.replace(/\u00a0/g, " ").replace(/\s+$/g, "");
|
|
14377
|
+
}
|
|
14378
|
+
function buildRowRefs(rows, method, totalRows, rowCount) {
|
|
14379
|
+
const refs = {};
|
|
14380
|
+
const firstVisibleRow = method === "buffer" ? Math.max(0, rowCount - totalRows) : 0;
|
|
14381
|
+
rows.forEach((text, index) => {
|
|
14382
|
+
refs[`@r${index}`] = {
|
|
14383
|
+
row: index,
|
|
14384
|
+
text,
|
|
14385
|
+
visible: method === "dom" ? true : index >= firstVisibleRow,
|
|
14386
|
+
selector: method === "dom" ? `#takumi-tui-dom-root [data-row="${index}"]` : undefined
|
|
14387
|
+
};
|
|
14388
|
+
});
|
|
14389
|
+
return refs;
|
|
14390
|
+
}
|
|
14391
|
+
async function configureDomRenderer(page, options) {
|
|
14392
|
+
await page.evaluate((opts) => {
|
|
14393
|
+
const runtimeKey = "__takumiTuiDomRenderer";
|
|
14394
|
+
const rootId = "takumi-tui-dom-root";
|
|
14395
|
+
const styleId = "takumi-tui-dom-style";
|
|
14396
|
+
const win = window;
|
|
14397
|
+
const ensureStyle = () => {
|
|
14398
|
+
let style = document.getElementById(styleId);
|
|
14399
|
+
if (!style) {
|
|
14400
|
+
style = document.createElement("style");
|
|
14401
|
+
style.id = styleId;
|
|
14402
|
+
document.head.appendChild(style);
|
|
14403
|
+
}
|
|
14404
|
+
style.textContent = `
|
|
14405
|
+
#${rootId} {
|
|
14406
|
+
position: absolute;
|
|
14407
|
+
inset: 0;
|
|
14408
|
+
overflow: hidden;
|
|
14409
|
+
display: flex;
|
|
14410
|
+
flex-direction: column;
|
|
14411
|
+
white-space: pre;
|
|
14412
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
14413
|
+
line-height: 1.2;
|
|
14414
|
+
background: var(--takumi-tui-bg, #1e1e1e);
|
|
14415
|
+
color: var(--takumi-tui-fg, #d4d4d4);
|
|
14416
|
+
z-index: 4;
|
|
14417
|
+
pointer-events: none;
|
|
14418
|
+
user-select: text;
|
|
14419
|
+
}
|
|
14420
|
+
#${rootId}[data-active="0"] {
|
|
14421
|
+
display: none;
|
|
14422
|
+
}
|
|
14423
|
+
#${rootId} .takumi-tui-dom-row {
|
|
14424
|
+
display: flex;
|
|
14425
|
+
min-height: 1.2em;
|
|
14426
|
+
}
|
|
14427
|
+
#${rootId} .takumi-tui-dom-cell {
|
|
14428
|
+
display: inline-flex;
|
|
14429
|
+
align-items: center;
|
|
14430
|
+
justify-content: center;
|
|
14431
|
+
min-width: 0.62em;
|
|
14432
|
+
height: 1.2em;
|
|
14433
|
+
}
|
|
14434
|
+
#${rootId} .takumi-tui-dom-cell[data-cursor="true"] {
|
|
14435
|
+
outline: 1px solid currentColor;
|
|
14436
|
+
outline-offset: -1px;
|
|
14437
|
+
}
|
|
14438
|
+
body[data-takumi-dom-render="1"] .xterm-rows,
|
|
14439
|
+
body[data-takumi-dom-render="1"] .xterm-text-layer,
|
|
14440
|
+
body[data-takumi-dom-render="1"] .xterm-cursor-layer,
|
|
14441
|
+
body[data-takumi-dom-render="1"] .xterm-selection-layer {
|
|
14442
|
+
opacity: 0 !important;
|
|
14443
|
+
}
|
|
14444
|
+
`;
|
|
14445
|
+
};
|
|
14446
|
+
const ensureRoot = () => {
|
|
14447
|
+
let root = document.getElementById(rootId);
|
|
14448
|
+
if (!root) {
|
|
14449
|
+
root = document.createElement("div");
|
|
14450
|
+
root.id = rootId;
|
|
14451
|
+
root.setAttribute("role", "grid");
|
|
14452
|
+
root.setAttribute("aria-label", "Terminal DOM renderer");
|
|
14453
|
+
const host = document.getElementById("terminal-container") ?? document.querySelector(".xterm") ?? document.body;
|
|
14454
|
+
if (getComputedStyle(host).position === "static") {
|
|
14455
|
+
host.style.position = "relative";
|
|
14456
|
+
}
|
|
14457
|
+
host.appendChild(root);
|
|
14458
|
+
}
|
|
14459
|
+
root.style.setProperty("--takumi-tui-bg", opts.theme === "light" ? "#ffffff" : "#1e1e1e");
|
|
14460
|
+
root.style.setProperty("--takumi-tui-fg", opts.theme === "light" ? "#1e1e1e" : "#d4d4d4");
|
|
14461
|
+
root.style.fontSize = `${opts.fontSize ?? 14}px`;
|
|
14462
|
+
root.dataset.active = opts.active ? "1" : "0";
|
|
14463
|
+
if (opts.active)
|
|
14464
|
+
root.removeAttribute("aria-hidden");
|
|
14465
|
+
else
|
|
14466
|
+
root.setAttribute("aria-hidden", "true");
|
|
14467
|
+
document.body.dataset.takumiDomRender = opts.active ? "1" : "0";
|
|
14468
|
+
return root;
|
|
14469
|
+
};
|
|
14470
|
+
const readCellChars = (line, col) => {
|
|
14471
|
+
try {
|
|
14472
|
+
const cell = typeof line?.getCell === "function" ? line.getCell(col) : null;
|
|
14473
|
+
const chars = typeof cell?.getChars === "function" ? cell.getChars() : "";
|
|
14474
|
+
if (chars)
|
|
14475
|
+
return chars;
|
|
14476
|
+
} catch {}
|
|
14477
|
+
try {
|
|
14478
|
+
const text = typeof line?.translateToString === "function" ? line.translateToString(false, col, col + 1) : "";
|
|
14479
|
+
if (text)
|
|
14480
|
+
return text;
|
|
14481
|
+
} catch {}
|
|
14482
|
+
return " ";
|
|
14483
|
+
};
|
|
14484
|
+
const buildState = (activeOnly) => {
|
|
14485
|
+
const term = win.term ?? win.terminal;
|
|
14486
|
+
if (!term?.buffer?.active) {
|
|
14487
|
+
return {
|
|
14488
|
+
text: "",
|
|
14489
|
+
rows: [],
|
|
14490
|
+
row_count: 0,
|
|
14491
|
+
cols: null,
|
|
14492
|
+
total_rows: 0,
|
|
14493
|
+
buffer_length: null,
|
|
14494
|
+
cursor_row: -1,
|
|
14495
|
+
cursor_col: -1,
|
|
14496
|
+
font_size: null,
|
|
14497
|
+
theme: opts.theme
|
|
14498
|
+
};
|
|
14499
|
+
}
|
|
14500
|
+
const buf = term.buffer.active;
|
|
14501
|
+
const rows = [];
|
|
14502
|
+
const root = ensureRoot();
|
|
14503
|
+
const fragment = document.createDocumentFragment();
|
|
14504
|
+
for (let row = 0;row < buf.length; row++) {
|
|
14505
|
+
const line = buf.getLine(row);
|
|
14506
|
+
if (!line)
|
|
14507
|
+
continue;
|
|
14508
|
+
const rowEl = document.createElement("div");
|
|
14509
|
+
rowEl.className = "takumi-tui-dom-row";
|
|
14510
|
+
rowEl.setAttribute("role", "row");
|
|
14511
|
+
rowEl.dataset.row = String(row);
|
|
14512
|
+
rowEl.setAttribute("aria-rowindex", String(row + 1));
|
|
14513
|
+
let rowText = "";
|
|
14514
|
+
for (let col = 0;col < term.cols; col++) {
|
|
14515
|
+
const char = readCellChars(line, col) || " ";
|
|
14516
|
+
rowText += char;
|
|
14517
|
+
const cellEl = document.createElement("span");
|
|
14518
|
+
cellEl.className = "takumi-tui-dom-cell";
|
|
14519
|
+
cellEl.setAttribute("role", "gridcell");
|
|
14520
|
+
cellEl.dataset.row = String(row);
|
|
14521
|
+
cellEl.dataset.col = String(col);
|
|
14522
|
+
cellEl.setAttribute("aria-colindex", String(col + 1));
|
|
14523
|
+
cellEl.textContent = char;
|
|
14524
|
+
if (buf.cursorY === row && buf.cursorX === col) {
|
|
14525
|
+
cellEl.dataset.cursor = "true";
|
|
14526
|
+
}
|
|
14527
|
+
rowEl.appendChild(cellEl);
|
|
14528
|
+
}
|
|
14529
|
+
rows.push(rowText.replace(/\s+$/g, ""));
|
|
14530
|
+
rowEl.setAttribute("aria-label", rows[rows.length - 1] || " ");
|
|
14531
|
+
fragment.appendChild(rowEl);
|
|
14532
|
+
}
|
|
14533
|
+
root.replaceChildren(fragment);
|
|
14534
|
+
root.setAttribute("aria-rowcount", String(rows.length));
|
|
14535
|
+
root.dataset.method = "dom";
|
|
14536
|
+
return {
|
|
14537
|
+
text: rows.join(`
|
|
14538
|
+
`).trimEnd(),
|
|
14539
|
+
rows,
|
|
14540
|
+
row_count: rows.length,
|
|
14541
|
+
cols: term.cols,
|
|
14542
|
+
total_rows: term.rows,
|
|
14543
|
+
buffer_length: buf.length,
|
|
14544
|
+
cursor_row: buf.cursorY,
|
|
14545
|
+
cursor_col: buf.cursorX,
|
|
14546
|
+
font_size: term.options?.fontSize ?? null,
|
|
14547
|
+
theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
|
|
14548
|
+
};
|
|
14549
|
+
};
|
|
14550
|
+
ensureStyle();
|
|
14551
|
+
ensureRoot();
|
|
14552
|
+
if (!win[runtimeKey]) {
|
|
14553
|
+
win[runtimeKey] = {
|
|
14554
|
+
sync: () => buildState(false),
|
|
14555
|
+
activate: (active) => {
|
|
14556
|
+
const root = ensureRoot();
|
|
14557
|
+
root.dataset.active = active ? "1" : "0";
|
|
14558
|
+
if (active)
|
|
14559
|
+
root.removeAttribute("aria-hidden");
|
|
14560
|
+
else
|
|
14561
|
+
root.setAttribute("aria-hidden", "true");
|
|
14562
|
+
document.body.dataset.takumiDomRender = active ? "1" : "0";
|
|
14563
|
+
}
|
|
14564
|
+
};
|
|
14565
|
+
const intervalId = window.setInterval(() => {
|
|
14566
|
+
try {
|
|
14567
|
+
win[runtimeKey]?.sync?.();
|
|
14568
|
+
} catch {}
|
|
14569
|
+
}, 50);
|
|
14570
|
+
win[runtimeKey].intervalId = intervalId;
|
|
14571
|
+
}
|
|
14572
|
+
win[runtimeKey].activate(opts.active);
|
|
14573
|
+
win[runtimeKey].sync();
|
|
14574
|
+
}, options);
|
|
14575
|
+
}
|
|
14576
|
+
async function destroyDomRenderer(page) {
|
|
14577
|
+
await page.evaluate(() => {
|
|
14578
|
+
const runtimeKey = "__takumiTuiDomRenderer";
|
|
14579
|
+
const win = window;
|
|
14580
|
+
if (win[runtimeKey]?.intervalId) {
|
|
14581
|
+
clearInterval(win[runtimeKey].intervalId);
|
|
14582
|
+
}
|
|
14583
|
+
delete win[runtimeKey];
|
|
14584
|
+
document.getElementById("takumi-tui-dom-root")?.remove();
|
|
14585
|
+
document.getElementById("takumi-tui-dom-style")?.remove();
|
|
14586
|
+
delete document.body.dataset.takumiDomRender;
|
|
14587
|
+
}).catch(() => {});
|
|
14588
|
+
}
|
|
14589
|
+
async function readDomMirrorState(page) {
|
|
14590
|
+
return page.evaluate(() => {
|
|
14591
|
+
const runtime = window.__takumiTuiDomRenderer;
|
|
14592
|
+
if (runtime?.sync)
|
|
14593
|
+
return runtime.sync();
|
|
14594
|
+
const rowEls = Array.from(document.querySelectorAll("#takumi-tui-dom-root [data-row]"));
|
|
14595
|
+
const rows = rowEls.map((row) => row.getAttribute("aria-label") ?? row.textContent ?? "");
|
|
14596
|
+
const term = window.term ?? window.terminal;
|
|
14597
|
+
const active = term?.buffer?.active;
|
|
14598
|
+
return {
|
|
14599
|
+
text: rows.join(`
|
|
14600
|
+
`).trimEnd(),
|
|
14601
|
+
rows,
|
|
14602
|
+
row_count: rows.length,
|
|
14603
|
+
cols: term?.cols ?? null,
|
|
14604
|
+
total_rows: term?.rows ?? rows.length,
|
|
14605
|
+
buffer_length: active?.length ?? rows.length,
|
|
14606
|
+
cursor_row: active?.cursorY ?? -1,
|
|
14607
|
+
cursor_col: active?.cursorX ?? -1,
|
|
14608
|
+
font_size: term?.options?.fontSize ?? null,
|
|
14609
|
+
theme: term?.options?.theme?.background === "#ffffff" ? "light" : "dark"
|
|
14610
|
+
};
|
|
14611
|
+
});
|
|
14612
|
+
}
|
|
14613
|
+
function isDomMethod(method) {
|
|
14614
|
+
return method === "dom";
|
|
14615
|
+
}
|
|
14616
|
+
async function withTimeout(label, operation, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
|
|
14617
|
+
let timedOut = false;
|
|
14618
|
+
const timer = setTimeout(() => {
|
|
14619
|
+
timedOut = true;
|
|
14620
|
+
}, timeoutMs);
|
|
14621
|
+
try {
|
|
14622
|
+
return await operation();
|
|
14623
|
+
} catch (err) {
|
|
14624
|
+
if (timedOut) {
|
|
14625
|
+
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");
|
|
14626
|
+
}
|
|
14627
|
+
throw err;
|
|
14628
|
+
} finally {
|
|
14629
|
+
clearTimeout(timer);
|
|
14630
|
+
}
|
|
14631
|
+
}
|
|
14632
|
+
async function isTuiHealthy(session) {
|
|
14633
|
+
const start = Date.now();
|
|
14634
|
+
try {
|
|
14635
|
+
await Promise.race([
|
|
14636
|
+
session.page.evaluate(() => {
|
|
14637
|
+
const term = window.term ?? window.terminal;
|
|
14638
|
+
if (!term)
|
|
14639
|
+
return false;
|
|
14640
|
+
if (!term.buffer?.active)
|
|
14641
|
+
return false;
|
|
14642
|
+
return true;
|
|
14643
|
+
}),
|
|
14644
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("health check timeout")), HEALTH_CHECK_TIMEOUT_MS))
|
|
14645
|
+
]);
|
|
14646
|
+
const latency = Date.now() - start;
|
|
14647
|
+
return { healthy: true, latency_ms: latency };
|
|
14648
|
+
} catch (err) {
|
|
14649
|
+
return { healthy: false, reason: err?.message ?? "unreachable" };
|
|
14650
|
+
}
|
|
14651
|
+
}
|
|
14652
|
+
async function reconnectTui(session, command, options = {}) {
|
|
14653
|
+
const port = session.port;
|
|
14654
|
+
try {
|
|
14655
|
+
session.ttydProcess.kill("SIGTERM");
|
|
14656
|
+
} catch {}
|
|
14657
|
+
try {
|
|
14658
|
+
await session.page.close();
|
|
14659
|
+
} catch {}
|
|
14660
|
+
try {
|
|
14661
|
+
await session.browser.close();
|
|
14662
|
+
} catch {}
|
|
14663
|
+
const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
|
|
14664
|
+
ttydProcess.on("error", (err) => {
|
|
14665
|
+
console.error(`[tui] reconnect ttyd error: ${err.message}`);
|
|
14666
|
+
});
|
|
14667
|
+
await waitForTtyd(port);
|
|
14668
|
+
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
14669
|
+
const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
|
|
14670
|
+
const page = await getPage(browser, { viewport });
|
|
14671
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
|
|
14672
|
+
await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
|
|
14673
|
+
let resolvedTheme = "dark";
|
|
14674
|
+
const req = options.theme ?? "dark";
|
|
14675
|
+
if (req === "light" || req === "dark") {
|
|
14676
|
+
resolvedTheme = req;
|
|
14677
|
+
} else {
|
|
14678
|
+
try {
|
|
14679
|
+
const r = execSync2("defaults read -g AppleInterfaceStyle 2>/dev/null", { encoding: "utf8" }).trim();
|
|
14680
|
+
resolvedTheme = r === "Dark" ? "dark" : "light";
|
|
14681
|
+
} catch {
|
|
14682
|
+
resolvedTheme = "light";
|
|
14683
|
+
}
|
|
14684
|
+
}
|
|
14685
|
+
const themeColors = THEMES[resolvedTheme];
|
|
14686
|
+
await page.evaluate((theme) => {
|
|
14687
|
+
const term = window.term ?? window.terminal;
|
|
14688
|
+
if (term?.options)
|
|
14689
|
+
term.options.theme = theme;
|
|
14690
|
+
document.body.style.backgroundColor = theme.background;
|
|
14691
|
+
}, themeColors);
|
|
14692
|
+
const method = options.method ?? session.method;
|
|
14693
|
+
await configureDomRenderer(page, {
|
|
14694
|
+
active: isDomMethod(method),
|
|
14695
|
+
theme: resolvedTheme,
|
|
14696
|
+
fontSize: options.fontSize
|
|
14697
|
+
});
|
|
14698
|
+
return {
|
|
14699
|
+
ttydProcess,
|
|
14700
|
+
port,
|
|
14701
|
+
browser,
|
|
14702
|
+
page,
|
|
14703
|
+
theme: resolvedTheme,
|
|
14704
|
+
method,
|
|
14705
|
+
lastHealthCheck: Date.now(),
|
|
14706
|
+
reconnectCount: session.reconnectCount + 1
|
|
14707
|
+
};
|
|
14708
|
+
}
|
|
14375
14709
|
function isTuiAvailable() {
|
|
14376
14710
|
try {
|
|
14377
14711
|
execSync2("which ttyd", { stdio: "ignore" });
|
|
@@ -14384,7 +14718,7 @@ async function findAvailablePort(startPort) {
|
|
|
14384
14718
|
let port = startPort;
|
|
14385
14719
|
for (let i = 0;i < 100; i++) {
|
|
14386
14720
|
try {
|
|
14387
|
-
|
|
14721
|
+
await fetch(`http://localhost:${port}`);
|
|
14388
14722
|
port++;
|
|
14389
14723
|
} catch {
|
|
14390
14724
|
return port;
|
|
@@ -14410,24 +14744,16 @@ async function launchTui(command, options = {}) {
|
|
|
14410
14744
|
}
|
|
14411
14745
|
const port = await findAvailablePort(nextPort);
|
|
14412
14746
|
nextPort = port + 1;
|
|
14413
|
-
const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], {
|
|
14414
|
-
stdio: "ignore",
|
|
14415
|
-
detached: false
|
|
14416
|
-
});
|
|
14747
|
+
const ttydProcess = spawn2("ttyd", ["--writable", "--port", String(port), "/bin/sh", "-c", command], { stdio: "ignore", detached: false });
|
|
14417
14748
|
ttydProcess.on("error", (err) => {
|
|
14418
14749
|
console.error(`[tui] ttyd process error: ${err.message}`);
|
|
14419
14750
|
});
|
|
14420
14751
|
try {
|
|
14421
14752
|
await waitForTtyd(port);
|
|
14422
14753
|
const viewport = options.viewport ?? { width: 1280, height: 720 };
|
|
14423
|
-
const browser = await launchPlaywright({
|
|
14424
|
-
headless: options.headless ?? true,
|
|
14425
|
-
viewport
|
|
14426
|
-
});
|
|
14754
|
+
const browser = await launchPlaywright({ headless: options.headless ?? true, viewport });
|
|
14427
14755
|
const page = await getPage(browser, { viewport });
|
|
14428
|
-
await page.goto(`http://localhost:${port}`, {
|
|
14429
|
-
waitUntil: "domcontentloaded"
|
|
14430
|
-
});
|
|
14756
|
+
await page.goto(`http://localhost:${port}`, { waitUntil: "domcontentloaded" });
|
|
14431
14757
|
await page.waitForSelector(".xterm-screen", { timeout: 1e4 });
|
|
14432
14758
|
let resolvedTheme = "dark";
|
|
14433
14759
|
const requestedTheme = options.theme ?? "system";
|
|
@@ -14446,16 +14772,15 @@ async function launchTui(command, options = {}) {
|
|
|
14446
14772
|
const themeColors = THEMES[resolvedTheme];
|
|
14447
14773
|
await page.evaluate((theme) => {
|
|
14448
14774
|
const term = window.term ?? window.terminal;
|
|
14449
|
-
if (term?.options)
|
|
14775
|
+
if (term?.options)
|
|
14450
14776
|
term.options.theme = theme;
|
|
14451
|
-
}
|
|
14452
14777
|
document.body.style.backgroundColor = theme.background;
|
|
14453
14778
|
const container = document.getElementById("terminal-container");
|
|
14454
14779
|
if (container)
|
|
14455
14780
|
container.style.backgroundColor = theme.background;
|
|
14456
|
-
const
|
|
14457
|
-
if (
|
|
14458
|
-
|
|
14781
|
+
const vp = document.querySelector(".xterm-viewport");
|
|
14782
|
+
if (vp)
|
|
14783
|
+
vp.style.backgroundColor = theme.background;
|
|
14459
14784
|
}, themeColors);
|
|
14460
14785
|
if (options.fontSize) {
|
|
14461
14786
|
await page.evaluate((size) => {
|
|
@@ -14464,13 +14789,115 @@ async function launchTui(command, options = {}) {
|
|
|
14464
14789
|
term.options.fontSize = size;
|
|
14465
14790
|
}, options.fontSize);
|
|
14466
14791
|
}
|
|
14467
|
-
|
|
14792
|
+
const method = options.method ?? "buffer";
|
|
14793
|
+
await configureDomRenderer(page, {
|
|
14794
|
+
active: isDomMethod(method),
|
|
14795
|
+
theme: resolvedTheme,
|
|
14796
|
+
fontSize: options.fontSize
|
|
14797
|
+
});
|
|
14798
|
+
return {
|
|
14799
|
+
ttydProcess,
|
|
14800
|
+
port,
|
|
14801
|
+
browser,
|
|
14802
|
+
page,
|
|
14803
|
+
theme: resolvedTheme,
|
|
14804
|
+
method,
|
|
14805
|
+
lastHealthCheck: Date.now(),
|
|
14806
|
+
reconnectCount: 0
|
|
14807
|
+
};
|
|
14468
14808
|
} catch (err) {
|
|
14469
|
-
|
|
14809
|
+
try {
|
|
14810
|
+
ttydProcess.kill("SIGTERM");
|
|
14811
|
+
} catch {}
|
|
14470
14812
|
throw err;
|
|
14471
14813
|
}
|
|
14472
14814
|
}
|
|
14815
|
+
async function getBufferState(page) {
|
|
14816
|
+
return page.evaluate(() => {
|
|
14817
|
+
const term = window.term ?? window.terminal;
|
|
14818
|
+
if (!term?.buffer?.active) {
|
|
14819
|
+
return {
|
|
14820
|
+
text: "",
|
|
14821
|
+
rows: [],
|
|
14822
|
+
row_count: 0,
|
|
14823
|
+
cols: null,
|
|
14824
|
+
total_rows: 0,
|
|
14825
|
+
buffer_length: null,
|
|
14826
|
+
cursor_row: -1,
|
|
14827
|
+
cursor_col: -1,
|
|
14828
|
+
font_size: null,
|
|
14829
|
+
theme: "dark"
|
|
14830
|
+
};
|
|
14831
|
+
}
|
|
14832
|
+
const buf = term.buffer.active;
|
|
14833
|
+
const rows = [];
|
|
14834
|
+
for (let i = 0;i < buf.length; i++) {
|
|
14835
|
+
const line = buf.getLine(i);
|
|
14836
|
+
if (line)
|
|
14837
|
+
rows.push(line.translateToString(true));
|
|
14838
|
+
}
|
|
14839
|
+
return {
|
|
14840
|
+
text: rows.join(`
|
|
14841
|
+
`).trimEnd(),
|
|
14842
|
+
rows,
|
|
14843
|
+
row_count: buf.length,
|
|
14844
|
+
cols: term.cols,
|
|
14845
|
+
total_rows: term.rows,
|
|
14846
|
+
buffer_length: buf.length,
|
|
14847
|
+
cursor_row: buf.cursorY,
|
|
14848
|
+
cursor_col: buf.cursorX,
|
|
14849
|
+
font_size: term.options?.fontSize ?? null,
|
|
14850
|
+
theme: term.options?.theme?.background === "#ffffff" ? "light" : "dark"
|
|
14851
|
+
};
|
|
14852
|
+
});
|
|
14853
|
+
}
|
|
14854
|
+
async function getDomState(page) {
|
|
14855
|
+
return readDomMirrorState(page);
|
|
14856
|
+
}
|
|
14857
|
+
async function getTerminalState(page, method = "buffer", timeoutMs = DEFAULT_TOOL_TIMEOUT_MS) {
|
|
14858
|
+
return withTimeout("getTerminalState", async () => {
|
|
14859
|
+
const raw = method === "dom" ? await getDomState(page) : await getBufferState(page);
|
|
14860
|
+
const rows = raw.rows.map(normalizeRowText);
|
|
14861
|
+
const text = rows.join(`
|
|
14862
|
+
`).trimEnd();
|
|
14863
|
+
return {
|
|
14864
|
+
...raw,
|
|
14865
|
+
method,
|
|
14866
|
+
rows,
|
|
14867
|
+
text,
|
|
14868
|
+
refs: buildRowRefs(rows, method, raw.total_rows, raw.row_count)
|
|
14869
|
+
};
|
|
14870
|
+
}, timeoutMs);
|
|
14871
|
+
}
|
|
14872
|
+
async function getTerminalText(page, timeoutMs = DEFAULT_TOOL_TIMEOUT_MS, method = "buffer") {
|
|
14873
|
+
const state = await getTerminalState(page, method, timeoutMs);
|
|
14874
|
+
return state.text;
|
|
14875
|
+
}
|
|
14876
|
+
async function waitForTerminalText(page, text, timeoutMs = 30000, method = "buffer") {
|
|
14877
|
+
const start = Date.now();
|
|
14878
|
+
while (Date.now() - start < timeoutMs) {
|
|
14879
|
+
let healthy = false;
|
|
14880
|
+
try {
|
|
14881
|
+
await Promise.race([
|
|
14882
|
+
page.evaluate(() => {
|
|
14883
|
+
const term = window.term ?? window.terminal;
|
|
14884
|
+
return term?.buffer?.active ? true : false;
|
|
14885
|
+
}),
|
|
14886
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("probe timeout")), 2000))
|
|
14887
|
+
]);
|
|
14888
|
+
healthy = true;
|
|
14889
|
+
} catch {}
|
|
14890
|
+
if (!healthy)
|
|
14891
|
+
return { found: false, elapsed_ms: Date.now() - start, stuck: true };
|
|
14892
|
+
const content = await getTerminalText(page, DEFAULT_TOOL_TIMEOUT_MS, method);
|
|
14893
|
+
if (content.includes(text))
|
|
14894
|
+
return { found: true, elapsed_ms: Date.now() - start, stuck: false };
|
|
14895
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
14896
|
+
}
|
|
14897
|
+
return { found: false, elapsed_ms: timeoutMs, stuck: false };
|
|
14898
|
+
}
|
|
14473
14899
|
async function closeTui(session) {
|
|
14900
|
+
await destroyDomRenderer(session.page);
|
|
14474
14901
|
try {
|
|
14475
14902
|
await session.page.close();
|
|
14476
14903
|
} catch {}
|
|
@@ -14480,8 +14907,11 @@ async function closeTui(session) {
|
|
|
14480
14907
|
try {
|
|
14481
14908
|
session.ttydProcess.kill("SIGTERM");
|
|
14482
14909
|
} catch {}
|
|
14910
|
+
try {
|
|
14911
|
+
session.ttydProcess.kill("SIGKILL");
|
|
14912
|
+
} catch {}
|
|
14483
14913
|
}
|
|
14484
|
-
var DEFAULT_TTYD_PORT_START = 7780, nextPort, THEMES;
|
|
14914
|
+
var DEFAULT_TTYD_PORT_START = 7780, nextPort, DEFAULT_TOOL_TIMEOUT_MS = 15000, HEALTH_CHECK_TIMEOUT_MS = 3000, THEMES;
|
|
14485
14915
|
var init_tui = __esm(() => {
|
|
14486
14916
|
init_types2();
|
|
14487
14917
|
init_playwright();
|
|
@@ -14811,19 +15241,13 @@ Object.defineProperty(navigator, 'plugins', {
|
|
|
14811
15241
|
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
14812
15242
|
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
|
|
14813
15243
|
];
|
|
14814
|
-
// Mimic PluginArray interface
|
|
14815
|
-
const pluginArray =
|
|
15244
|
+
// Mimic PluginArray interface \u2014 guard against removed prototypes
|
|
15245
|
+
const pluginArray = {};
|
|
14816
15246
|
plugins.forEach((p, i) => {
|
|
14817
|
-
const plugin =
|
|
14818
|
-
Object.defineProperties(plugin, {
|
|
14819
|
-
name: { value: p.name, enumerable: true },
|
|
14820
|
-
filename: { value: p.filename, enumerable: true },
|
|
14821
|
-
description: { value: p.description, enumerable: true },
|
|
14822
|
-
length: { value: p.length, enumerable: true },
|
|
14823
|
-
});
|
|
15247
|
+
const plugin = { ...p, item: () => null };
|
|
14824
15248
|
pluginArray[i] = plugin;
|
|
14825
15249
|
});
|
|
14826
|
-
|
|
15250
|
+
pluginArray.length = plugins.length;
|
|
14827
15251
|
pluginArray.item = (i) => pluginArray[i] || null;
|
|
14828
15252
|
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
|
|
14829
15253
|
pluginArray.refresh = () => {};
|
|
@@ -15768,6 +16192,7 @@ function watchPage(page, opts) {
|
|
|
15768
16192
|
const changes = [];
|
|
15769
16193
|
const intervalMs = opts?.intervalMs ?? 500;
|
|
15770
16194
|
const maxChanges = opts?.maxChanges ?? 50;
|
|
16195
|
+
const sessionId = opts?.sessionId;
|
|
15771
16196
|
const interval = setInterval(async () => {
|
|
15772
16197
|
if (changes.length >= maxChanges)
|
|
15773
16198
|
return;
|
|
@@ -15781,7 +16206,7 @@ function watchPage(page, opts) {
|
|
|
15781
16206
|
}
|
|
15782
16207
|
} catch {}
|
|
15783
16208
|
}, intervalMs);
|
|
15784
|
-
activeWatches.set(id, { interval, changes });
|
|
16209
|
+
activeWatches.set(id, { interval, changes, sessionId });
|
|
15785
16210
|
return {
|
|
15786
16211
|
id,
|
|
15787
16212
|
stop: () => {
|
|
@@ -15800,10 +16225,12 @@ function stopWatch(watchId) {
|
|
|
15800
16225
|
activeWatches.delete(watchId);
|
|
15801
16226
|
}
|
|
15802
16227
|
}
|
|
15803
|
-
function stopAllWatchesForSession(
|
|
15804
|
-
for (const [id, w] of activeWatches) {
|
|
15805
|
-
|
|
15806
|
-
|
|
16228
|
+
function stopAllWatchesForSession(sessionId) {
|
|
16229
|
+
for (const [id, w] of [...activeWatches]) {
|
|
16230
|
+
if (!sessionId || w.sessionId === sessionId) {
|
|
16231
|
+
clearInterval(w.interval);
|
|
16232
|
+
activeWatches.delete(id);
|
|
16233
|
+
}
|
|
15807
16234
|
}
|
|
15808
16235
|
}
|
|
15809
16236
|
async function clickRef(page, sessionId, ref, opts) {
|
|
@@ -15884,6 +16311,7 @@ var init_actions = __esm(() => {
|
|
|
15884
16311
|
// src/lib/session.ts
|
|
15885
16312
|
var exports_session = {};
|
|
15886
16313
|
__export(exports_session, {
|
|
16314
|
+
setSessionTui: () => setSessionTui,
|
|
15887
16315
|
setSessionPage: () => setSessionPage,
|
|
15888
16316
|
renameSession: () => renameSession2,
|
|
15889
16317
|
listSessions: () => listSessions2,
|
|
@@ -15891,8 +16319,10 @@ __export(exports_session, {
|
|
|
15891
16319
|
isAutoGallery: () => isAutoGallery,
|
|
15892
16320
|
hasActiveHandle: () => hasActiveHandle,
|
|
15893
16321
|
getTokenBudget: () => getTokenBudget,
|
|
16322
|
+
getSessionTuiSession: () => getSessionTuiSession,
|
|
15894
16323
|
getSessionPage: () => getSessionPage,
|
|
15895
16324
|
getSessionEngine: () => getSessionEngine,
|
|
16325
|
+
getSessionCommand: () => getSessionCommand,
|
|
15896
16326
|
getSessionByName: () => getSessionByName2,
|
|
15897
16327
|
getSessionBunView: () => getSessionBunView,
|
|
15898
16328
|
getSessionBrowser: () => getSessionBrowser,
|
|
@@ -15938,7 +16368,7 @@ async function createSession2(opts = {}) {
|
|
|
15938
16368
|
try {
|
|
15939
16369
|
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
15940
16370
|
} catch {}
|
|
15941
|
-
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 });
|
|
16371
|
+
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 ?? "" });
|
|
15942
16372
|
return { session: session2, page: page2 };
|
|
15943
16373
|
}
|
|
15944
16374
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
@@ -15952,13 +16382,29 @@ async function createSession2(opts = {}) {
|
|
|
15952
16382
|
browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
|
|
15953
16383
|
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
15954
16384
|
} else {
|
|
15955
|
-
|
|
16385
|
+
const testView = new BunWebViewSession({
|
|
15956
16386
|
width: opts.viewport?.width ?? 1280,
|
|
15957
16387
|
height: opts.viewport?.height ?? 720,
|
|
15958
16388
|
profile: opts.name ?? undefined
|
|
15959
16389
|
});
|
|
15960
|
-
|
|
15961
|
-
|
|
16390
|
+
let bunWorks = true;
|
|
16391
|
+
try {
|
|
16392
|
+
await testView.goto("data:text/html,<html></html>");
|
|
16393
|
+
} catch {
|
|
16394
|
+
bunWorks = false;
|
|
16395
|
+
try {
|
|
16396
|
+
await testView.close();
|
|
16397
|
+
} catch {}
|
|
16398
|
+
}
|
|
16399
|
+
if (!bunWorks) {
|
|
16400
|
+
console.warn("[browser] Bun.WebView exists but Chrome not available \u2014 falling back to playwright");
|
|
16401
|
+
browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
|
|
16402
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
16403
|
+
} else {
|
|
16404
|
+
bunView = testView;
|
|
16405
|
+
if (opts.stealth) {}
|
|
16406
|
+
page = createBunProxy(bunView);
|
|
16407
|
+
}
|
|
15962
16408
|
}
|
|
15963
16409
|
} else if (resolvedEngine === "lightpanda") {
|
|
15964
16410
|
browser = await connectLightpanda();
|
|
@@ -15970,7 +16416,8 @@ async function createSession2(opts = {}) {
|
|
|
15970
16416
|
headless: opts.headless ?? true,
|
|
15971
16417
|
viewport: opts.viewport,
|
|
15972
16418
|
theme: opts.tuiTheme ?? "system",
|
|
15973
|
-
fontSize: opts.tuiFontSize
|
|
16419
|
+
fontSize: opts.tuiFontSize,
|
|
16420
|
+
method: opts.tuiMethod ?? "buffer"
|
|
15974
16421
|
});
|
|
15975
16422
|
browser = tuiSess.browser;
|
|
15976
16423
|
page = tuiSess.page;
|
|
@@ -15996,7 +16443,7 @@ async function createSession2(opts = {}) {
|
|
|
15996
16443
|
try {
|
|
15997
16444
|
cleanups2.push(setupDialogHandler(page, session2.id));
|
|
15998
16445
|
} catch {}
|
|
15999
|
-
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 });
|
|
16446
|
+
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" });
|
|
16000
16447
|
return { session: session2, page };
|
|
16001
16448
|
} else {
|
|
16002
16449
|
browser = await pool.acquire(opts.headless ?? true);
|
|
@@ -16068,7 +16515,7 @@ async function createSession2(opts = {}) {
|
|
|
16068
16515
|
} catch {}
|
|
16069
16516
|
}
|
|
16070
16517
|
}
|
|
16071
|
-
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 });
|
|
16518
|
+
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 ?? "" });
|
|
16072
16519
|
if (opts.startUrl) {
|
|
16073
16520
|
try {
|
|
16074
16521
|
if (bunView) {
|
|
@@ -16120,6 +16567,23 @@ function getSessionEngine(sessionId) {
|
|
|
16120
16567
|
function hasActiveHandle(sessionId) {
|
|
16121
16568
|
return handles.has(sessionId);
|
|
16122
16569
|
}
|
|
16570
|
+
function getSessionTuiSession(sessionId) {
|
|
16571
|
+
return handles.get(sessionId)?.tuiSession ?? null;
|
|
16572
|
+
}
|
|
16573
|
+
function setSessionTui(sessionId, tuiSess) {
|
|
16574
|
+
const handle = handles.get(sessionId);
|
|
16575
|
+
if (!handle)
|
|
16576
|
+
throw new SessionNotFoundError(sessionId);
|
|
16577
|
+
handle.tuiSession = tuiSess;
|
|
16578
|
+
handle.page = tuiSess.page;
|
|
16579
|
+
if (tuiSess.browser !== handle.browser) {
|
|
16580
|
+
handle.browser = tuiSess.browser;
|
|
16581
|
+
}
|
|
16582
|
+
handle.lastActivity = Date.now();
|
|
16583
|
+
}
|
|
16584
|
+
function getSessionCommand(sessionId) {
|
|
16585
|
+
return handles.get(sessionId)?.startUrl ?? "bash";
|
|
16586
|
+
}
|
|
16123
16587
|
function setSessionPage(sessionId, page) {
|
|
16124
16588
|
const handle = handles.get(sessionId);
|
|
16125
16589
|
if (!handle)
|
|
@@ -16128,38 +16592,43 @@ function setSessionPage(sessionId, page) {
|
|
|
16128
16592
|
}
|
|
16129
16593
|
async function closeSession2(sessionId) {
|
|
16130
16594
|
const handle = handles.get(sessionId);
|
|
16131
|
-
|
|
16132
|
-
|
|
16133
|
-
|
|
16134
|
-
|
|
16135
|
-
|
|
16136
|
-
|
|
16137
|
-
|
|
16138
|
-
|
|
16139
|
-
|
|
16140
|
-
|
|
16141
|
-
|
|
16142
|
-
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16595
|
+
try {
|
|
16596
|
+
if (handle) {
|
|
16597
|
+
for (const cleanup of handle.cleanups) {
|
|
16598
|
+
try {
|
|
16599
|
+
cleanup();
|
|
16600
|
+
} catch {}
|
|
16601
|
+
}
|
|
16602
|
+
if (handle.bunView) {
|
|
16603
|
+
try {
|
|
16604
|
+
await handle.bunView.close();
|
|
16605
|
+
} catch {}
|
|
16606
|
+
} else if (handle.tuiSession) {} else {
|
|
16607
|
+
try {
|
|
16608
|
+
await handle.page.context().close();
|
|
16609
|
+
} catch {}
|
|
16610
|
+
try {
|
|
16611
|
+
if (handle.browser)
|
|
16612
|
+
pool.release(handle.browser);
|
|
16613
|
+
} catch {}
|
|
16614
|
+
}
|
|
16147
16615
|
}
|
|
16616
|
+
try {
|
|
16617
|
+
const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
16618
|
+
clearLastSnapshot2(sessionId);
|
|
16619
|
+
clearSessionRefs2(sessionId);
|
|
16620
|
+
} catch {}
|
|
16621
|
+
try {
|
|
16622
|
+
const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
|
|
16623
|
+
stopAllWatchesForSession2(sessionId);
|
|
16624
|
+
} catch {}
|
|
16625
|
+
try {
|
|
16626
|
+
const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
|
|
16627
|
+
clearDialogs2(sessionId);
|
|
16628
|
+
} catch {}
|
|
16629
|
+
} finally {
|
|
16148
16630
|
handles.delete(sessionId);
|
|
16149
16631
|
}
|
|
16150
|
-
try {
|
|
16151
|
-
const { clearLastSnapshot: clearLastSnapshot2, clearSessionRefs: clearSessionRefs2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
16152
|
-
clearLastSnapshot2(sessionId);
|
|
16153
|
-
clearSessionRefs2(sessionId);
|
|
16154
|
-
} catch {}
|
|
16155
|
-
try {
|
|
16156
|
-
const { stopAllWatchesForSession: stopAllWatchesForSession2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
|
|
16157
|
-
stopAllWatchesForSession2(sessionId);
|
|
16158
|
-
} catch {}
|
|
16159
|
-
try {
|
|
16160
|
-
const { clearDialogs: clearDialogs2 } = await Promise.resolve().then(() => (init_dialogs(), exports_dialogs));
|
|
16161
|
-
clearDialogs2(sessionId);
|
|
16162
|
-
} catch {}
|
|
16163
16632
|
return closeSession(sessionId);
|
|
16164
16633
|
}
|
|
16165
16634
|
function getSession2(sessionId) {
|
|
@@ -16260,14 +16729,16 @@ var init_session = __esm(() => {
|
|
|
16260
16729
|
ttlInterval.unref();
|
|
16261
16730
|
DB_PRUNE_INTERVAL_MS = 30 * 60000;
|
|
16262
16731
|
dbPruneInterval = setInterval(() => {
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16732
|
+
(async () => {
|
|
16733
|
+
try {
|
|
16734
|
+
const { getDatabase: getDatabase2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
16735
|
+
const db = getDatabase2();
|
|
16736
|
+
const cutoff = new Date(Date.now() - DB_RETENTION_HOURS * 3600000).toISOString();
|
|
16737
|
+
db.prepare("DELETE FROM network_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
|
|
16738
|
+
db.prepare("DELETE FROM console_log WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
|
|
16739
|
+
db.prepare("DELETE FROM snapshots WHERE session_id IN (SELECT id FROM sessions WHERE status != 'active') AND timestamp < ?").run(cutoff);
|
|
16740
|
+
} catch {}
|
|
16741
|
+
})();
|
|
16271
16742
|
}, DB_PRUNE_INTERVAL_MS);
|
|
16272
16743
|
if (dbPruneInterval.unref)
|
|
16273
16744
|
dbPruneInterval.unref();
|
|
@@ -22864,6 +23335,12 @@ __export(exports_gallery, {
|
|
|
22864
23335
|
createEntry: () => createEntry
|
|
22865
23336
|
});
|
|
22866
23337
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
23338
|
+
function validateDataPath(filePath) {
|
|
23339
|
+
if (filePath.includes("..")) {
|
|
23340
|
+
throw new Error(`File path must not contain '..': ${filePath}`);
|
|
23341
|
+
}
|
|
23342
|
+
return filePath;
|
|
23343
|
+
}
|
|
22867
23344
|
function deserialize(row) {
|
|
22868
23345
|
return {
|
|
22869
23346
|
id: row.id,
|
|
@@ -22888,6 +23365,9 @@ function deserialize(row) {
|
|
|
22888
23365
|
function createEntry(data) {
|
|
22889
23366
|
const db = getDatabase();
|
|
22890
23367
|
const id = randomUUID4();
|
|
23368
|
+
validateDataPath(data.path);
|
|
23369
|
+
if (data.thumbnail_path)
|
|
23370
|
+
validateDataPath(data.thumbnail_path);
|
|
22891
23371
|
db.prepare(`
|
|
22892
23372
|
INSERT INTO gallery_entries
|
|
22893
23373
|
(id, session_id, project_id, url, title, path, thumbnail_path, format,
|
|
@@ -24214,11 +24694,21 @@ async function execBrowser(cfg, step, page, vars) {
|
|
|
24214
24694
|
break;
|
|
24215
24695
|
}
|
|
24216
24696
|
}
|
|
24697
|
+
function isValidArg(arg) {
|
|
24698
|
+
return /^[a-zA-Z0-9._\-/@:]+$/.test(arg);
|
|
24699
|
+
}
|
|
24217
24700
|
async function execConnector(cfg, step, vars) {
|
|
24218
24701
|
const connector = cfg.connector;
|
|
24219
24702
|
if (!connector)
|
|
24220
24703
|
throw new Error("Connector step missing 'connector' in config");
|
|
24221
|
-
|
|
24704
|
+
if (!ALLOWED_CONNECTORS.has(connector)) {
|
|
24705
|
+
throw new Error(`Unknown connector '${connector}'. Allowed: ${[...ALLOWED_CONNECTORS].join(", ")}`);
|
|
24706
|
+
}
|
|
24707
|
+
const args = (cfg.args ?? []).filter((a) => typeof a === "string").filter((a) => {
|
|
24708
|
+
if (!isValidArg(a))
|
|
24709
|
+
throw new Error(`Connector arg '${a}' contains disallowed characters`);
|
|
24710
|
+
return true;
|
|
24711
|
+
});
|
|
24222
24712
|
const proc = Bun.spawn([`connect-${connector}`, ...args], {
|
|
24223
24713
|
stdout: "pipe",
|
|
24224
24714
|
stderr: "pipe",
|
|
@@ -24296,9 +24786,11 @@ async function aiSelfHeal(page, description, step) {
|
|
|
24296
24786
|
function decodeHtmlEntities(str) {
|
|
24297
24787
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'");
|
|
24298
24788
|
}
|
|
24789
|
+
var ALLOWED_CONNECTORS;
|
|
24299
24790
|
var init_script_engine = __esm(() => {
|
|
24300
24791
|
init_scripts();
|
|
24301
24792
|
init_ai_inference();
|
|
24793
|
+
ALLOWED_CONNECTORS = new Set(["github", "linear", "slack", "jira", "notion", "gitlab"]);
|
|
24302
24794
|
});
|
|
24303
24795
|
|
|
24304
24796
|
// src/db/agents.ts
|
|
@@ -24857,11 +25349,14 @@ import { join as join14 } from "path";
|
|
|
24857
25349
|
import { homedir as homedir8 } from "os";
|
|
24858
25350
|
async function getCredentials(service) {
|
|
24859
25351
|
try {
|
|
24860
|
-
const
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
25352
|
+
const secretsVaultPath = process.env["BROWSER_SECRETS_VAULT_PATH"];
|
|
25353
|
+
if (secretsVaultPath) {
|
|
25354
|
+
const { getSecret } = await import(secretsVaultPath);
|
|
25355
|
+
const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
|
|
25356
|
+
const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
|
|
25357
|
+
if (email?.value && password?.value) {
|
|
25358
|
+
return { email: email.value, password: password.value };
|
|
25359
|
+
}
|
|
24865
25360
|
}
|
|
24866
25361
|
} catch {}
|
|
24867
25362
|
const secretsPath = join14(homedir8(), ".secrets");
|
|
@@ -29417,7 +29912,7 @@ ENGINES:
|
|
|
29417
29912
|
- "cdp": Chrome DevTools Protocol \u2014 network monitoring, perf profiling, script injection
|
|
29418
29913
|
- "lightpanda": fast headless for static pages
|
|
29419
29914
|
- "bun": native Bun.WebView \u2014 fastest for screenshots and scraping
|
|
29420
|
-
- "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.
|
|
29915
|
+
- "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.
|
|
29421
29916
|
|
|
29422
29917
|
TIPS:
|
|
29423
29918
|
- If agent_id is set and already has an active session, returns the existing one (use force_new to override)
|
|
@@ -29439,8 +29934,9 @@ TIPS:
|
|
|
29439
29934
|
tags: exports_external2.array(exports_external2.string()).optional(),
|
|
29440
29935
|
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"),
|
|
29441
29936
|
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."),
|
|
29442
|
-
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.")
|
|
29443
|
-
|
|
29937
|
+
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."),
|
|
29938
|
+
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.")
|
|
29939
|
+
}, 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 }) => {
|
|
29444
29940
|
try {
|
|
29445
29941
|
if (agent_id && !force_new) {
|
|
29446
29942
|
const existing = getActiveSessionForAgent2(agent_id);
|
|
@@ -29460,7 +29956,8 @@ TIPS:
|
|
|
29460
29956
|
storageState: storage_state,
|
|
29461
29957
|
cdpUrl: cdp_url,
|
|
29462
29958
|
tuiTheme: tui_theme,
|
|
29463
|
-
tuiFontSize: tui_font_size
|
|
29959
|
+
tuiFontSize: tui_font_size,
|
|
29960
|
+
tuiMethod: tui_method
|
|
29464
29961
|
});
|
|
29465
29962
|
if (tags?.length) {
|
|
29466
29963
|
const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -29918,6 +30415,13 @@ function register5(server) {
|
|
|
29918
30415
|
});
|
|
29919
30416
|
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 }) => {
|
|
29920
30417
|
try {
|
|
30418
|
+
if (file_path.includes("..")) {
|
|
30419
|
+
return err(new Error("File path must not contain '..'"));
|
|
30420
|
+
}
|
|
30421
|
+
const { existsSync: existsSync8 } = await import("fs");
|
|
30422
|
+
if (!existsSync8(file_path)) {
|
|
30423
|
+
return err(new Error(`File not found: ${file_path}`));
|
|
30424
|
+
}
|
|
29921
30425
|
const sid = resolveSessionId(session_id);
|
|
29922
30426
|
const page = getSessionPage(sid);
|
|
29923
30427
|
await uploadFile(page, selector, file_path);
|
|
@@ -30631,14 +31135,14 @@ function register6(server) {
|
|
|
30631
31135
|
} else if (/^title\s+contains\s+/i.test(trimmed)) {
|
|
30632
31136
|
const needle = trimmed.replace(/^title\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
|
|
30633
31137
|
result = (await getTitle(page)).toLowerCase().includes(needle.toLowerCase());
|
|
30634
|
-
} else if (/^text:["'](
|
|
30635
|
-
const text = trimmed.match(/^text:["'](
|
|
31138
|
+
} else if (/^text:["']([^"']+)["']/i.test(trimmed)) {
|
|
31139
|
+
const text = trimmed.match(/^text:["']([^"']+)["']/i)?.[1] ?? "";
|
|
30636
31140
|
result = await page.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)}) ?? false`);
|
|
30637
31141
|
} else if (/^element:["'](.+)["']/i.test(trimmed)) {
|
|
30638
31142
|
const sel = trimmed.match(/^element:["'](.+)["']/i)?.[1] ?? "";
|
|
30639
31143
|
result = await page.evaluate(`!!document.querySelector(${JSON.stringify(sel)})`);
|
|
30640
|
-
} else if (/^count:["'](
|
|
30641
|
-
const [, sel, op, n] = trimmed.match(/^count:["'](
|
|
31144
|
+
} else if (/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i.test(trimmed)) {
|
|
31145
|
+
const [, sel, op, n] = trimmed.match(/^count:["']([^"']+)["']\s*([><=!]+)\s*(\d+)/i);
|
|
30642
31146
|
const count = await page.evaluate(`document.querySelectorAll(${JSON.stringify(sel)}).length`);
|
|
30643
31147
|
const num = parseInt(n);
|
|
30644
31148
|
result = op === ">" ? count > num : op === ">=" ? count >= num : op === "<" ? count < num : op === "<=" ? count <= num : count === num;
|
|
@@ -34577,6 +35081,220 @@ var init_scripts2 = __esm(() => {
|
|
|
34577
35081
|
init_helpers();
|
|
34578
35082
|
});
|
|
34579
35083
|
|
|
35084
|
+
// src/mcp/agents.ts
|
|
35085
|
+
function registerAgentsAndProjects(server) {
|
|
35086
|
+
server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
|
|
35087
|
+
name: exports_external2.string(),
|
|
35088
|
+
description: exports_external2.string().optional(),
|
|
35089
|
+
session_id: exports_external2.string().optional(),
|
|
35090
|
+
project_id: exports_external2.string().optional(),
|
|
35091
|
+
working_dir: exports_external2.string().optional()
|
|
35092
|
+
}, async ({ name, description, session_id, project_id, working_dir }) => {
|
|
35093
|
+
try {
|
|
35094
|
+
const agent = registerAgent2(name, { description, sessionId: session_id, projectId: project_id, workingDir: working_dir });
|
|
35095
|
+
return json({ agent });
|
|
35096
|
+
} catch (e) {
|
|
35097
|
+
return err(e);
|
|
35098
|
+
}
|
|
35099
|
+
});
|
|
35100
|
+
server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external2.string() }, async ({ agent_id }) => {
|
|
35101
|
+
try {
|
|
35102
|
+
heartbeat2(agent_id);
|
|
35103
|
+
return json({ ok: true, agent_id, timestamp: new Date().toISOString() });
|
|
35104
|
+
} catch (e) {
|
|
35105
|
+
return err(e);
|
|
35106
|
+
}
|
|
35107
|
+
});
|
|
35108
|
+
server.tool("list_agents", "List all registered agents.", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
|
|
35109
|
+
try {
|
|
35110
|
+
return json({ agents: listAgents(project_id) });
|
|
35111
|
+
} catch (e) {
|
|
35112
|
+
return err(e);
|
|
35113
|
+
}
|
|
35114
|
+
});
|
|
35115
|
+
server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external2.string(), project_id: exports_external2.string().optional() }, async ({ agent_id, project_id }) => {
|
|
35116
|
+
try {
|
|
35117
|
+
const { updateAgent: update } = await Promise.resolve().then(() => (init_agents2(), exports_agents2));
|
|
35118
|
+
update(agent_id, { project_id: project_id ?? undefined });
|
|
35119
|
+
return json({ ok: true, agent_id, project_id });
|
|
35120
|
+
} catch (e) {
|
|
35121
|
+
return err(e);
|
|
35122
|
+
}
|
|
35123
|
+
});
|
|
35124
|
+
server.tool("browser_project_create", "Create or ensure a project exists", { name: exports_external2.string(), path: exports_external2.string(), description: exports_external2.string().optional() }, async ({ name, path, description }) => {
|
|
35125
|
+
try {
|
|
35126
|
+
const project = ensureProject(name, path, description);
|
|
35127
|
+
return json({ project });
|
|
35128
|
+
} catch (e) {
|
|
35129
|
+
return err(e);
|
|
35130
|
+
}
|
|
35131
|
+
});
|
|
35132
|
+
server.tool("browser_project_list", "List all registered projects", {}, async () => {
|
|
35133
|
+
try {
|
|
35134
|
+
return json({ projects: listProjects() });
|
|
35135
|
+
} catch (e) {
|
|
35136
|
+
return err(e);
|
|
35137
|
+
}
|
|
35138
|
+
});
|
|
35139
|
+
}
|
|
35140
|
+
var init_agents3 = __esm(() => {
|
|
35141
|
+
init_helpers();
|
|
35142
|
+
});
|
|
35143
|
+
|
|
35144
|
+
// src/mcp/gallery.ts
|
|
35145
|
+
function registerGalleryAndDownloads(server) {
|
|
35146
|
+
server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
|
|
35147
|
+
project_id: exports_external2.string().optional(),
|
|
35148
|
+
session_id: exports_external2.string().optional(),
|
|
35149
|
+
tag: exports_external2.string().optional(),
|
|
35150
|
+
is_favorite: exports_external2.boolean().optional(),
|
|
35151
|
+
date_from: exports_external2.string().optional(),
|
|
35152
|
+
date_to: exports_external2.string().optional(),
|
|
35153
|
+
limit: exports_external2.number().optional().default(50),
|
|
35154
|
+
offset: exports_external2.number().optional().default(0)
|
|
35155
|
+
}, async ({ project_id, session_id, tag, is_favorite, date_from, date_to, limit, offset }) => {
|
|
35156
|
+
try {
|
|
35157
|
+
const entries = listEntries({ projectId: project_id, sessionId: session_id, tag, isFavorite: is_favorite, dateFrom: date_from, dateTo: date_to, limit, offset });
|
|
35158
|
+
return json({ entries, count: entries.length });
|
|
35159
|
+
} catch (e) {
|
|
35160
|
+
return err(e);
|
|
35161
|
+
}
|
|
35162
|
+
});
|
|
35163
|
+
server.tool("browser_gallery_get", "Get a gallery entry by id, including thumbnail base64", { id: exports_external2.string() }, async ({ id }) => {
|
|
35164
|
+
try {
|
|
35165
|
+
const entry = getEntry(id);
|
|
35166
|
+
if (!entry)
|
|
35167
|
+
return err(new Error(`Gallery entry not found: ${id}`));
|
|
35168
|
+
let thumbnail_base64;
|
|
35169
|
+
if (entry.thumbnail_path) {
|
|
35170
|
+
try {
|
|
35171
|
+
thumbnail_base64 = Buffer.from(await Bun.file(entry.thumbnail_path).arrayBuffer()).toString("base64");
|
|
35172
|
+
} catch {}
|
|
35173
|
+
}
|
|
35174
|
+
return json({ entry, thumbnail_base64 });
|
|
35175
|
+
} catch (e) {
|
|
35176
|
+
return err(e);
|
|
35177
|
+
}
|
|
35178
|
+
});
|
|
35179
|
+
server.tool("browser_gallery_tag", "Add a tag to a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
|
|
35180
|
+
try {
|
|
35181
|
+
return json({ entry: tagEntry(id, tag) });
|
|
35182
|
+
} catch (e) {
|
|
35183
|
+
return err(e);
|
|
35184
|
+
}
|
|
35185
|
+
});
|
|
35186
|
+
server.tool("browser_gallery_untag", "Remove a tag from a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
|
|
35187
|
+
try {
|
|
35188
|
+
return json({ entry: untagEntry(id, tag) });
|
|
35189
|
+
} catch (e) {
|
|
35190
|
+
return err(e);
|
|
35191
|
+
}
|
|
35192
|
+
});
|
|
35193
|
+
server.tool("browser_gallery_favorite", "Mark or unmark a gallery entry as favorite", { id: exports_external2.string(), favorited: exports_external2.boolean() }, async ({ id, favorited }) => {
|
|
35194
|
+
try {
|
|
35195
|
+
return json({ entry: favoriteEntry(id, favorited) });
|
|
35196
|
+
} catch (e) {
|
|
35197
|
+
return err(e);
|
|
35198
|
+
}
|
|
35199
|
+
});
|
|
35200
|
+
server.tool("browser_gallery_delete", "Delete a gallery entry", { id: exports_external2.string() }, async ({ id }) => {
|
|
35201
|
+
try {
|
|
35202
|
+
deleteEntry(id);
|
|
35203
|
+
return json({ deleted: id });
|
|
35204
|
+
} catch (e) {
|
|
35205
|
+
return err(e);
|
|
35206
|
+
}
|
|
35207
|
+
});
|
|
35208
|
+
server.tool("browser_gallery_search", "Search gallery entries by url, title, notes, or tags", { q: exports_external2.string(), limit: exports_external2.number().optional().default(20) }, async ({ q, limit }) => {
|
|
35209
|
+
try {
|
|
35210
|
+
return json({ entries: searchEntries(q, limit) });
|
|
35211
|
+
} catch (e) {
|
|
35212
|
+
return err(e);
|
|
35213
|
+
}
|
|
35214
|
+
});
|
|
35215
|
+
server.tool("browser_gallery_stats", "Get gallery statistics: total, size, favorites, by-format breakdown", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
|
|
35216
|
+
try {
|
|
35217
|
+
return json(getGalleryStats(project_id));
|
|
35218
|
+
} catch (e) {
|
|
35219
|
+
return err(e);
|
|
35220
|
+
}
|
|
35221
|
+
});
|
|
35222
|
+
server.tool("browser_gallery_diff", "Pixel-diff two gallery screenshots. Returns diff image base64 + changed pixel count.", { id1: exports_external2.string(), id2: exports_external2.string() }, async ({ id1, id2 }) => {
|
|
35223
|
+
try {
|
|
35224
|
+
const e1 = getEntry(id1);
|
|
35225
|
+
const e2 = getEntry(id2);
|
|
35226
|
+
if (!e1)
|
|
35227
|
+
return err(new Error(`Gallery entry not found: ${id1}`));
|
|
35228
|
+
if (!e2)
|
|
35229
|
+
return err(new Error(`Gallery entry not found: ${id2}`));
|
|
35230
|
+
const result = await diffImages(e1.path, e2.path);
|
|
35231
|
+
return json(result);
|
|
35232
|
+
} catch (e) {
|
|
35233
|
+
return err(e);
|
|
35234
|
+
}
|
|
35235
|
+
});
|
|
35236
|
+
server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
|
|
35237
|
+
try {
|
|
35238
|
+
return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
|
|
35239
|
+
} catch (e) {
|
|
35240
|
+
return err(e);
|
|
35241
|
+
}
|
|
35242
|
+
});
|
|
35243
|
+
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 }) => {
|
|
35244
|
+
try {
|
|
35245
|
+
const file = getDownload(id, session_id);
|
|
35246
|
+
if (!file)
|
|
35247
|
+
return err(new Error(`Download not found: ${id}`));
|
|
35248
|
+
const base64 = Buffer.from(await Bun.file(file.path).arrayBuffer()).toString("base64");
|
|
35249
|
+
return json({ file, base64 });
|
|
35250
|
+
} catch (e) {
|
|
35251
|
+
return err(e);
|
|
35252
|
+
}
|
|
35253
|
+
});
|
|
35254
|
+
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 }) => {
|
|
35255
|
+
try {
|
|
35256
|
+
return json({ deleted: deleteDownload(id, session_id) });
|
|
35257
|
+
} catch (e) {
|
|
35258
|
+
return err(e);
|
|
35259
|
+
}
|
|
35260
|
+
});
|
|
35261
|
+
server.tool("browser_downloads_clean", "Delete all downloaded files older than N days (default 7)", { older_than_days: exports_external2.number().optional().default(7) }, async ({ older_than_days }) => {
|
|
35262
|
+
try {
|
|
35263
|
+
return json({ deleted_count: cleanStaleDownloads(older_than_days) });
|
|
35264
|
+
} catch (e) {
|
|
35265
|
+
return err(e);
|
|
35266
|
+
}
|
|
35267
|
+
});
|
|
35268
|
+
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 }) => {
|
|
35269
|
+
try {
|
|
35270
|
+
const finalPath = exportToPath(id, target_path, session_id);
|
|
35271
|
+
return json({ path: finalPath });
|
|
35272
|
+
} catch (e) {
|
|
35273
|
+
return err(e);
|
|
35274
|
+
}
|
|
35275
|
+
});
|
|
35276
|
+
server.tool("browser_persist_file", "Persist a file permanently via open-files SDK (or local fallback)", { download_id: exports_external2.string().optional(), path: exports_external2.string().optional(), project_id: exports_external2.string().optional(), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ download_id, path: filePath, project_id, tags }) => {
|
|
35277
|
+
try {
|
|
35278
|
+
let localPath = filePath;
|
|
35279
|
+
if (download_id) {
|
|
35280
|
+
const file = getDownload(download_id);
|
|
35281
|
+
if (!file)
|
|
35282
|
+
return err(new Error(`Download not found: ${download_id}`));
|
|
35283
|
+
localPath = file.path;
|
|
35284
|
+
}
|
|
35285
|
+
if (!localPath)
|
|
35286
|
+
return err(new Error("Either download_id or path is required"));
|
|
35287
|
+
const result = await persistFile(localPath, { projectId: project_id, tags });
|
|
35288
|
+
return json(result);
|
|
35289
|
+
} catch (e) {
|
|
35290
|
+
return err(e);
|
|
35291
|
+
}
|
|
35292
|
+
});
|
|
35293
|
+
}
|
|
35294
|
+
var init_gallery2 = __esm(() => {
|
|
35295
|
+
init_helpers();
|
|
35296
|
+
});
|
|
35297
|
+
|
|
34580
35298
|
// node_modules/@hasna/mementos/dist/index.js
|
|
34581
35299
|
var exports_dist3 = {};
|
|
34582
35300
|
__export(exports_dist3, {
|
|
@@ -80076,209 +80794,8 @@ Use "done" when the task is complete with {"result": "the answer"}.
|
|
|
80076
80794
|
Keep actions simple and focused. Prefer evaluate for data extraction.`;
|
|
80077
80795
|
var init_ai_task = () => {};
|
|
80078
80796
|
|
|
80079
|
-
// src/mcp/
|
|
80080
|
-
function
|
|
80081
|
-
server.tool("register_agent", "Register an agent session. Returns agent_id. Auto-triggers a heartbeat.", {
|
|
80082
|
-
name: exports_external2.string(),
|
|
80083
|
-
description: exports_external2.string().optional(),
|
|
80084
|
-
session_id: exports_external2.string().optional().optional(),
|
|
80085
|
-
project_id: exports_external2.string().optional(),
|
|
80086
|
-
working_dir: exports_external2.string().optional()
|
|
80087
|
-
}, async ({ name, description, session_id, project_id, working_dir }) => {
|
|
80088
|
-
try {
|
|
80089
|
-
const agent = registerAgent2(name, { description, sessionId: session_id, projectId: project_id, workingDir: working_dir });
|
|
80090
|
-
return json({ agent });
|
|
80091
|
-
} catch (e) {
|
|
80092
|
-
return err(e);
|
|
80093
|
-
}
|
|
80094
|
-
});
|
|
80095
|
-
server.tool("heartbeat", "Update last_seen_at to signal agent is active.", { agent_id: exports_external2.string() }, async ({ agent_id }) => {
|
|
80096
|
-
try {
|
|
80097
|
-
heartbeat2(agent_id);
|
|
80098
|
-
return json({ ok: true, agent_id, timestamp: new Date().toISOString() });
|
|
80099
|
-
} catch (e) {
|
|
80100
|
-
return err(e);
|
|
80101
|
-
}
|
|
80102
|
-
});
|
|
80103
|
-
server.tool("list_agents", "List all registered agents.", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
|
|
80104
|
-
try {
|
|
80105
|
-
return json({ agents: listAgents(project_id) });
|
|
80106
|
-
} catch (e) {
|
|
80107
|
-
return err(e);
|
|
80108
|
-
}
|
|
80109
|
-
});
|
|
80110
|
-
server.tool("set_focus", "Set active project context for this agent session.", { agent_id: exports_external2.string(), project_id: exports_external2.string().optional() }, async ({ agent_id, project_id }) => {
|
|
80111
|
-
try {
|
|
80112
|
-
const { updateAgent: update } = await Promise.resolve().then(() => (init_agents2(), exports_agents2));
|
|
80113
|
-
update(agent_id, { project_id: project_id ?? undefined });
|
|
80114
|
-
return json({ ok: true, agent_id, project_id });
|
|
80115
|
-
} catch (e) {
|
|
80116
|
-
return err(e);
|
|
80117
|
-
}
|
|
80118
|
-
});
|
|
80119
|
-
server.tool("browser_project_create", "Create or ensure a project exists", { name: exports_external2.string(), path: exports_external2.string(), description: exports_external2.string().optional() }, async ({ name, path, description }) => {
|
|
80120
|
-
try {
|
|
80121
|
-
const project = ensureProject(name, path, description);
|
|
80122
|
-
return json({ project });
|
|
80123
|
-
} catch (e) {
|
|
80124
|
-
return err(e);
|
|
80125
|
-
}
|
|
80126
|
-
});
|
|
80127
|
-
server.tool("browser_project_list", "List all registered projects", {}, async () => {
|
|
80128
|
-
try {
|
|
80129
|
-
return json({ projects: listProjects() });
|
|
80130
|
-
} catch (e) {
|
|
80131
|
-
return err(e);
|
|
80132
|
-
}
|
|
80133
|
-
});
|
|
80134
|
-
server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
|
|
80135
|
-
project_id: exports_external2.string().optional(),
|
|
80136
|
-
session_id: exports_external2.string().optional().optional(),
|
|
80137
|
-
tag: exports_external2.string().optional(),
|
|
80138
|
-
is_favorite: exports_external2.boolean().optional(),
|
|
80139
|
-
date_from: exports_external2.string().optional(),
|
|
80140
|
-
date_to: exports_external2.string().optional(),
|
|
80141
|
-
limit: exports_external2.number().optional().default(50),
|
|
80142
|
-
offset: exports_external2.number().optional().default(0)
|
|
80143
|
-
}, async ({ project_id, session_id, tag, is_favorite, date_from, date_to, limit, offset }) => {
|
|
80144
|
-
try {
|
|
80145
|
-
const entries = listEntries({ projectId: project_id, sessionId: session_id, tag, isFavorite: is_favorite, dateFrom: date_from, dateTo: date_to, limit, offset });
|
|
80146
|
-
return json({ entries, count: entries.length });
|
|
80147
|
-
} catch (e) {
|
|
80148
|
-
return err(e);
|
|
80149
|
-
}
|
|
80150
|
-
});
|
|
80151
|
-
server.tool("browser_gallery_get", "Get a gallery entry by id, including thumbnail base64", { id: exports_external2.string() }, async ({ id }) => {
|
|
80152
|
-
try {
|
|
80153
|
-
const entry = getEntry(id);
|
|
80154
|
-
if (!entry)
|
|
80155
|
-
return err(new Error(`Gallery entry not found: ${id}`));
|
|
80156
|
-
let thumbnail_base64;
|
|
80157
|
-
if (entry.thumbnail_path) {
|
|
80158
|
-
try {
|
|
80159
|
-
thumbnail_base64 = Buffer.from(await Bun.file(entry.thumbnail_path).arrayBuffer()).toString("base64");
|
|
80160
|
-
} catch {}
|
|
80161
|
-
}
|
|
80162
|
-
return json({ entry, thumbnail_base64 });
|
|
80163
|
-
} catch (e) {
|
|
80164
|
-
return err(e);
|
|
80165
|
-
}
|
|
80166
|
-
});
|
|
80167
|
-
server.tool("browser_gallery_tag", "Add a tag to a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
|
|
80168
|
-
try {
|
|
80169
|
-
return json({ entry: tagEntry(id, tag) });
|
|
80170
|
-
} catch (e) {
|
|
80171
|
-
return err(e);
|
|
80172
|
-
}
|
|
80173
|
-
});
|
|
80174
|
-
server.tool("browser_gallery_untag", "Remove a tag from a gallery entry", { id: exports_external2.string(), tag: exports_external2.string() }, async ({ id, tag }) => {
|
|
80175
|
-
try {
|
|
80176
|
-
return json({ entry: untagEntry(id, tag) });
|
|
80177
|
-
} catch (e) {
|
|
80178
|
-
return err(e);
|
|
80179
|
-
}
|
|
80180
|
-
});
|
|
80181
|
-
server.tool("browser_gallery_favorite", "Mark or unmark a gallery entry as favorite", { id: exports_external2.string(), favorited: exports_external2.boolean() }, async ({ id, favorited }) => {
|
|
80182
|
-
try {
|
|
80183
|
-
return json({ entry: favoriteEntry(id, favorited) });
|
|
80184
|
-
} catch (e) {
|
|
80185
|
-
return err(e);
|
|
80186
|
-
}
|
|
80187
|
-
});
|
|
80188
|
-
server.tool("browser_gallery_delete", "Delete a gallery entry", { id: exports_external2.string() }, async ({ id }) => {
|
|
80189
|
-
try {
|
|
80190
|
-
deleteEntry(id);
|
|
80191
|
-
return json({ deleted: id });
|
|
80192
|
-
} catch (e) {
|
|
80193
|
-
return err(e);
|
|
80194
|
-
}
|
|
80195
|
-
});
|
|
80196
|
-
server.tool("browser_gallery_search", "Search gallery entries by url, title, notes, or tags", { q: exports_external2.string(), limit: exports_external2.number().optional().default(20) }, async ({ q, limit }) => {
|
|
80197
|
-
try {
|
|
80198
|
-
return json({ entries: searchEntries(q, limit) });
|
|
80199
|
-
} catch (e) {
|
|
80200
|
-
return err(e);
|
|
80201
|
-
}
|
|
80202
|
-
});
|
|
80203
|
-
server.tool("browser_gallery_stats", "Get gallery statistics: total, size, favorites, by-format breakdown", { project_id: exports_external2.string().optional() }, async ({ project_id }) => {
|
|
80204
|
-
try {
|
|
80205
|
-
return json(getGalleryStats(project_id));
|
|
80206
|
-
} catch (e) {
|
|
80207
|
-
return err(e);
|
|
80208
|
-
}
|
|
80209
|
-
});
|
|
80210
|
-
server.tool("browser_gallery_diff", "Pixel-diff two gallery screenshots. Returns diff image base64 + changed pixel count.", { id1: exports_external2.string(), id2: exports_external2.string() }, async ({ id1, id2 }) => {
|
|
80211
|
-
try {
|
|
80212
|
-
const e1 = getEntry(id1);
|
|
80213
|
-
const e2 = getEntry(id2);
|
|
80214
|
-
if (!e1)
|
|
80215
|
-
return err(new Error(`Gallery entry not found: ${id1}`));
|
|
80216
|
-
if (!e2)
|
|
80217
|
-
return err(new Error(`Gallery entry not found: ${id2}`));
|
|
80218
|
-
const result = await diffImages(e1.path, e2.path);
|
|
80219
|
-
return json(result);
|
|
80220
|
-
} catch (e) {
|
|
80221
|
-
return err(e);
|
|
80222
|
-
}
|
|
80223
|
-
});
|
|
80224
|
-
server.tool("browser_downloads_list", "List all files in the downloads folder", { session_id: exports_external2.string().optional().optional() }, async ({ session_id }) => {
|
|
80225
|
-
try {
|
|
80226
|
-
return json({ downloads: listDownloads(session_id), count: listDownloads(session_id).length });
|
|
80227
|
-
} catch (e) {
|
|
80228
|
-
return err(e);
|
|
80229
|
-
}
|
|
80230
|
-
});
|
|
80231
|
-
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().optional() }, async ({ id, session_id }) => {
|
|
80232
|
-
try {
|
|
80233
|
-
const file = getDownload(id, session_id);
|
|
80234
|
-
if (!file)
|
|
80235
|
-
return err(new Error(`Download not found: ${id}`));
|
|
80236
|
-
const base64 = Buffer.from(await Bun.file(file.path).arrayBuffer()).toString("base64");
|
|
80237
|
-
return json({ file, base64 });
|
|
80238
|
-
} catch (e) {
|
|
80239
|
-
return err(e);
|
|
80240
|
-
}
|
|
80241
|
-
});
|
|
80242
|
-
server.tool("browser_downloads_delete", "Delete a downloaded file by id", { id: exports_external2.string(), session_id: exports_external2.string().optional().optional() }, async ({ id, session_id }) => {
|
|
80243
|
-
try {
|
|
80244
|
-
const deleted = deleteDownload(id, session_id);
|
|
80245
|
-
return json({ deleted });
|
|
80246
|
-
} catch (e) {
|
|
80247
|
-
return err(e);
|
|
80248
|
-
}
|
|
80249
|
-
});
|
|
80250
|
-
server.tool("browser_downloads_clean", "Delete all downloaded files older than N days (default 7)", { older_than_days: exports_external2.number().optional().default(7) }, async ({ older_than_days }) => {
|
|
80251
|
-
try {
|
|
80252
|
-
return json({ deleted_count: cleanStaleDownloads(older_than_days) });
|
|
80253
|
-
} catch (e) {
|
|
80254
|
-
return err(e);
|
|
80255
|
-
}
|
|
80256
|
-
});
|
|
80257
|
-
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().optional() }, async ({ id, target_path, session_id }) => {
|
|
80258
|
-
try {
|
|
80259
|
-
const finalPath = exportToPath(id, target_path, session_id);
|
|
80260
|
-
return json({ path: finalPath });
|
|
80261
|
-
} catch (e) {
|
|
80262
|
-
return err(e);
|
|
80263
|
-
}
|
|
80264
|
-
});
|
|
80265
|
-
server.tool("browser_persist_file", "Persist a file permanently via open-files SDK (or local fallback)", { download_id: exports_external2.string().optional(), path: exports_external2.string().optional(), project_id: exports_external2.string().optional(), tags: exports_external2.array(exports_external2.string()).optional() }, async ({ download_id, path: filePath, project_id, tags }) => {
|
|
80266
|
-
try {
|
|
80267
|
-
let localPath = filePath;
|
|
80268
|
-
if (download_id) {
|
|
80269
|
-
const file = getDownload(download_id);
|
|
80270
|
-
if (!file)
|
|
80271
|
-
return err(new Error(`Download not found: ${download_id}`));
|
|
80272
|
-
localPath = file.path;
|
|
80273
|
-
}
|
|
80274
|
-
if (!localPath)
|
|
80275
|
-
return err(new Error("Either download_id or path is required"));
|
|
80276
|
-
const result = await persistFile(localPath, { projectId: project_id, tags });
|
|
80277
|
-
return json(result);
|
|
80278
|
-
} catch (e) {
|
|
80279
|
-
return err(e);
|
|
80280
|
-
}
|
|
80281
|
-
});
|
|
80797
|
+
// src/mcp/integration.ts
|
|
80798
|
+
function registerIntegrationAndMeta(server) {
|
|
80282
80799
|
const activeWatchHandles2 = new Map;
|
|
80283
80800
|
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 }) => {
|
|
80284
80801
|
try {
|
|
@@ -80308,214 +80825,7 @@ function register10(server) {
|
|
|
80308
80825
|
return err(e);
|
|
80309
80826
|
}
|
|
80310
80827
|
});
|
|
80311
|
-
server.tool("
|
|
80312
|
-
try {
|
|
80313
|
-
const groups = {
|
|
80314
|
-
Navigation: [
|
|
80315
|
-
{ tool: "browser_navigate", description: "Navigate to a URL" },
|
|
80316
|
-
{ tool: "browser_back", description: "Navigate back in history" },
|
|
80317
|
-
{ tool: "browser_forward", description: "Navigate forward in history" },
|
|
80318
|
-
{ tool: "browser_reload", description: "Reload the current page" },
|
|
80319
|
-
{ tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
|
|
80320
|
-
{ tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
|
|
80321
|
-
],
|
|
80322
|
-
Interaction: [
|
|
80323
|
-
{ tool: "browser_click", description: "Click element by ref or selector" },
|
|
80324
|
-
{ tool: "browser_click_text", description: "Click element by visible text" },
|
|
80325
|
-
{ tool: "browser_type", description: "Type text into an element" },
|
|
80326
|
-
{ tool: "browser_hover", description: "Hover over an element" },
|
|
80327
|
-
{ tool: "browser_scroll", description: "Scroll the page" },
|
|
80328
|
-
{ tool: "browser_select", description: "Select a dropdown option" },
|
|
80329
|
-
{ tool: "browser_toggle", description: "Check/uncheck a checkbox" },
|
|
80330
|
-
{ tool: "browser_upload", description: "Upload a file to an input" },
|
|
80331
|
-
{ tool: "browser_press_key", description: "Press a keyboard key" },
|
|
80332
|
-
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
80333
|
-
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
80334
|
-
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
80335
|
-
{ tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
|
|
80336
|
-
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
80337
|
-
],
|
|
80338
|
-
Extraction: [
|
|
80339
|
-
{ tool: "browser_get_text", description: "Get text content from page/selector" },
|
|
80340
|
-
{ tool: "browser_get_html", description: "Get HTML content from page/selector" },
|
|
80341
|
-
{ tool: "browser_get_links", description: "Get all links on the page" },
|
|
80342
|
-
{ tool: "browser_get_page_info", description: "Full page summary in one call" },
|
|
80343
|
-
{ tool: "browser_extract", description: "Extract content in various formats" },
|
|
80344
|
-
{ tool: "browser_find", description: "Find elements by selector" },
|
|
80345
|
-
{ tool: "browser_element_exists", description: "Check if a selector exists" },
|
|
80346
|
-
{ tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
|
|
80347
|
-
{ tool: "browser_evaluate", description: "Execute JavaScript in page context" }
|
|
80348
|
-
],
|
|
80349
|
-
Capture: [
|
|
80350
|
-
{ tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
|
|
80351
|
-
{ tool: "browser_pdf", description: "Generate a PDF of the page" },
|
|
80352
|
-
{ tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
|
|
80353
|
-
{ tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" },
|
|
80354
|
-
{ tool: "browser_diff", description: "Visual diff between two URLs \u2014 highlights changes in red" }
|
|
80355
|
-
],
|
|
80356
|
-
Storage: [
|
|
80357
|
-
{ tool: "browser_cookies_get", description: "Get cookies" },
|
|
80358
|
-
{ tool: "browser_cookies_set", description: "Set a cookie" },
|
|
80359
|
-
{ tool: "browser_cookies_clear", description: "Clear cookies" },
|
|
80360
|
-
{ tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
|
|
80361
|
-
{ tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
|
|
80362
|
-
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
80363
|
-
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
80364
|
-
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
80365
|
-
{ tool: "browser_profile_delete", description: "Delete a saved profile" },
|
|
80366
|
-
{ tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
|
|
80367
|
-
{ tool: "browser_session_list_states", description: "List saved storage states" },
|
|
80368
|
-
{ tool: "browser_session_delete_state", description: "Delete a saved storage state" }
|
|
80369
|
-
],
|
|
80370
|
-
Network: [
|
|
80371
|
-
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
80372
|
-
{ tool: "browser_network_intercept", description: "Add a network interception rule" },
|
|
80373
|
-
{ tool: "browser_har_start", description: "Start HAR capture" },
|
|
80374
|
-
{ tool: "browser_har_stop", description: "Stop HAR capture and get data" },
|
|
80375
|
-
{ tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
|
|
80376
|
-
{ tool: "browser_intercept_clear", description: "Remove all response intercepts" }
|
|
80377
|
-
],
|
|
80378
|
-
Performance: [
|
|
80379
|
-
{ tool: "browser_performance", description: "Get performance metrics" },
|
|
80380
|
-
{ tool: "browser_performance_budget", description: "Check perf against budget thresholds (LCP, FCP, CLS, TTFB)" }
|
|
80381
|
-
],
|
|
80382
|
-
Console: [
|
|
80383
|
-
{ tool: "browser_console_log", description: "Get console messages" },
|
|
80384
|
-
{ tool: "browser_has_errors", description: "Check for console errors" },
|
|
80385
|
-
{ tool: "browser_clear_errors", description: "Clear console error log" },
|
|
80386
|
-
{ tool: "browser_get_dialogs", description: "Get pending dialogs" }
|
|
80387
|
-
],
|
|
80388
|
-
Recording: [
|
|
80389
|
-
{ tool: "browser_record_start", description: "Start recording actions" },
|
|
80390
|
-
{ tool: "browser_record_step", description: "Add a step to recording" },
|
|
80391
|
-
{ tool: "browser_record_stop", description: "Stop and save recording" },
|
|
80392
|
-
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
80393
|
-
{ tool: "browser_record_export", description: "Export recording as Playwright test, Puppeteer script, or JSON" },
|
|
80394
|
-
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
80395
|
-
],
|
|
80396
|
-
Auth: [
|
|
80397
|
-
{ tool: "browser_auth_record", description: "Start recording a login flow" },
|
|
80398
|
-
{ tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
|
|
80399
|
-
{ tool: "browser_auth_replay", description: "Replay a saved auth flow" },
|
|
80400
|
-
{ tool: "browser_auth_list", description: "List all saved auth flows" },
|
|
80401
|
-
{ tool: "browser_auth_delete", description: "Delete a saved auth flow" }
|
|
80402
|
-
],
|
|
80403
|
-
Workflows: [
|
|
80404
|
-
{ tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
|
|
80405
|
-
{ tool: "browser_workflow_list", description: "List all saved workflows" },
|
|
80406
|
-
{ tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
|
|
80407
|
-
{ tool: "browser_workflow_delete", description: "Delete a saved workflow" }
|
|
80408
|
-
],
|
|
80409
|
-
Data: [
|
|
80410
|
-
{ tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
|
|
80411
|
-
{ tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
|
|
80412
|
-
{ tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
|
|
80413
|
-
{ tool: "browser_dataset_list", description: "List all saved datasets" },
|
|
80414
|
-
{ tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
|
|
80415
|
-
{ tool: "browser_dataset_delete", description: "Delete a saved dataset" }
|
|
80416
|
-
],
|
|
80417
|
-
Crawl: [
|
|
80418
|
-
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
80419
|
-
],
|
|
80420
|
-
Agent: [
|
|
80421
|
-
{ tool: "register_agent", description: "Register an agent session" },
|
|
80422
|
-
{ tool: "heartbeat", description: "Update agent last_seen_at" },
|
|
80423
|
-
{ tool: "list_agents", description: "List registered agents" },
|
|
80424
|
-
{ tool: "set_focus", description: "Set active project context" }
|
|
80425
|
-
],
|
|
80426
|
-
Project: [
|
|
80427
|
-
{ tool: "browser_project_create", description: "Create or ensure a project" },
|
|
80428
|
-
{ tool: "browser_project_list", description: "List all projects" }
|
|
80429
|
-
],
|
|
80430
|
-
Gallery: [
|
|
80431
|
-
{ tool: "browser_gallery_list", description: "List screenshot gallery entries" },
|
|
80432
|
-
{ tool: "browser_gallery_get", description: "Get a gallery entry by id" },
|
|
80433
|
-
{ tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
|
|
80434
|
-
{ tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
|
|
80435
|
-
{ tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
|
|
80436
|
-
{ tool: "browser_gallery_delete", description: "Delete a gallery entry" },
|
|
80437
|
-
{ tool: "browser_gallery_search", description: "Search gallery entries" },
|
|
80438
|
-
{ tool: "browser_gallery_stats", description: "Get gallery statistics" },
|
|
80439
|
-
{ tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
|
|
80440
|
-
],
|
|
80441
|
-
Downloads: [
|
|
80442
|
-
{ tool: "browser_downloads_list", description: "List downloaded files" },
|
|
80443
|
-
{ tool: "browser_downloads_get", description: "Get a download by id" },
|
|
80444
|
-
{ tool: "browser_downloads_delete", description: "Delete a download" },
|
|
80445
|
-
{ tool: "browser_downloads_clean", description: "Clean old downloads" },
|
|
80446
|
-
{ tool: "browser_downloads_export", description: "Copy download to a path" },
|
|
80447
|
-
{ tool: "browser_persist_file", description: "Persist file permanently" }
|
|
80448
|
-
],
|
|
80449
|
-
Session: [
|
|
80450
|
-
{ tool: "browser_session_create", description: "Create a new browser session" },
|
|
80451
|
-
{ tool: "browser_session_list", description: "List all sessions" },
|
|
80452
|
-
{ tool: "browser_session_close", description: "Close a session" },
|
|
80453
|
-
{ tool: "browser_session_get_by_name", description: "Get session by name" },
|
|
80454
|
-
{ tool: "browser_session_rename", description: "Rename a session" },
|
|
80455
|
-
{ tool: "browser_session_lock", description: "Lock a session for an agent" },
|
|
80456
|
-
{ tool: "browser_session_unlock", description: "Unlock a session" },
|
|
80457
|
-
{ tool: "browser_session_transfer", description: "Transfer session to another agent" },
|
|
80458
|
-
{ tool: "browser_session_tag", description: "Add a tag to a session" },
|
|
80459
|
-
{ tool: "browser_session_untag", description: "Remove a tag from a session" },
|
|
80460
|
-
{ tool: "browser_session_stats", description: "Get session stats and token usage" },
|
|
80461
|
-
{ tool: "browser_session_timeline", description: "Get chronological action log" },
|
|
80462
|
-
{ tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
|
|
80463
|
-
{ tool: "browser_tab_new", description: "Open a new tab" },
|
|
80464
|
-
{ tool: "browser_tab_list", description: "List all open tabs" },
|
|
80465
|
-
{ tool: "browser_tab_switch", description: "Switch to a tab by index" },
|
|
80466
|
-
{ tool: "browser_tab_close", description: "Close a tab by index" }
|
|
80467
|
-
],
|
|
80468
|
-
TUI: [
|
|
80469
|
-
{ tool: "browser_tui_send_keys", description: "Send keystrokes (ctrl+c, arrow_up, tab, enter, etc.)" },
|
|
80470
|
-
{ tool: "browser_tui_send_text", description: "Type text + optional Enter (most common TUI interaction)" },
|
|
80471
|
-
{ tool: "browser_tui_resize", description: "Resize terminal cols/rows mid-session" },
|
|
80472
|
-
{ tool: "browser_tui_get_text", description: "Get terminal text buffer (full or row range)" },
|
|
80473
|
-
{ tool: "browser_tui_wait_for_text", description: "Wait for text to appear in terminal output" },
|
|
80474
|
-
{ tool: "browser_tui_get_cursor", description: "Get cursor position (row, col)" },
|
|
80475
|
-
{ tool: "browser_tui_assert", description: "Assert terminal conditions (text contains, row N contains, cursor at)" },
|
|
80476
|
-
{ tool: "browser_tui_snapshot", description: "Structured terminal snapshot (rows array, cursor, dimensions)" },
|
|
80477
|
-
{ tool: "browser_tui_record_start", description: "Start recording terminal as asciicast" },
|
|
80478
|
-
{ tool: "browser_tui_record_stop", description: "Stop recording, return asciicast v2 JSON" }
|
|
80479
|
-
],
|
|
80480
|
-
Meta: [
|
|
80481
|
-
{ tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
|
|
80482
|
-
{ tool: "browser_version", description: "Show running binary version and tool count" },
|
|
80483
|
-
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
80484
|
-
{ tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
|
|
80485
|
-
{ tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
|
|
80486
|
-
{ tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
|
|
80487
|
-
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
80488
|
-
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
80489
|
-
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
80490
|
-
{ tool: "browser_watch_stop", description: "Stop DOM watcher" },
|
|
80491
|
-
{ tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
|
|
80492
|
-
]
|
|
80493
|
-
};
|
|
80494
|
-
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
80495
|
-
return json({ groups, total_tools: totalTools });
|
|
80496
|
-
} catch (e) {
|
|
80497
|
-
return err(e);
|
|
80498
|
-
}
|
|
80499
|
-
});
|
|
80500
|
-
server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
|
|
80501
|
-
try {
|
|
80502
|
-
const { getDataDir: getDataDir7 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
80503
|
-
const toolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
80504
|
-
const { readFileSync: readFileSync12 } = await import("fs");
|
|
80505
|
-
const { join: join27 } = await import("path");
|
|
80506
|
-
const _pkg = JSON.parse(readFileSync12(join27(import.meta.dir, "../../package.json"), "utf8"));
|
|
80507
|
-
return json({
|
|
80508
|
-
version: _pkg.version,
|
|
80509
|
-
mcp_tools_count: toolCount,
|
|
80510
|
-
bun_version: Bun.version,
|
|
80511
|
-
data_dir: getDataDir7(),
|
|
80512
|
-
node_env: process.env["NODE_ENV"] ?? "production"
|
|
80513
|
-
});
|
|
80514
|
-
} catch (e) {
|
|
80515
|
-
return err(e);
|
|
80516
|
-
}
|
|
80517
|
-
});
|
|
80518
|
-
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 }) => {
|
|
80828
|
+
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 }) => {
|
|
80519
80829
|
try {
|
|
80520
80830
|
const sid = resolveSessionId(session_id);
|
|
80521
80831
|
const page = getSessionPage(sid);
|
|
@@ -80532,7 +80842,7 @@ function register10(server) {
|
|
|
80532
80842
|
return err(e);
|
|
80533
80843
|
}
|
|
80534
80844
|
});
|
|
80535
|
-
server.tool("browser_remember", "Store page facts in open-mementos for future recall.
|
|
80845
|
+
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 }) => {
|
|
80536
80846
|
try {
|
|
80537
80847
|
const sid = resolveSessionId(session_id);
|
|
80538
80848
|
const page = getSessionPage(sid);
|
|
@@ -80544,7 +80854,7 @@ function register10(server) {
|
|
|
80544
80854
|
return err(e);
|
|
80545
80855
|
}
|
|
80546
80856
|
});
|
|
80547
|
-
server.tool("browser_recall", "Retrieve cached page facts from open-mementos.
|
|
80857
|
+
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 }) => {
|
|
80548
80858
|
try {
|
|
80549
80859
|
const { recallPage: recallPage2 } = await Promise.resolve().then(() => (init_page_memory(), exports_page_memory));
|
|
80550
80860
|
const memory = await recallPage2(url, max_age_hours);
|
|
@@ -80565,7 +80875,7 @@ function register10(server) {
|
|
|
80565
80875
|
return err(e);
|
|
80566
80876
|
}
|
|
80567
80877
|
});
|
|
80568
|
-
server.tool("browser_check_navigation", "Check if another agent is already scraping this URL.
|
|
80878
|
+
server.tool("browser_check_navigation", "Check if another agent is already scraping this URL.", { url: exports_external2.string() }, async ({ url }) => {
|
|
80569
80879
|
try {
|
|
80570
80880
|
const { checkDuplicate: checkDuplicate3 } = await Promise.resolve().then(() => (init_coordination(), exports_coordination));
|
|
80571
80881
|
return json(await checkDuplicate3(url));
|
|
@@ -80599,7 +80909,7 @@ function register10(server) {
|
|
|
80599
80909
|
return err(e);
|
|
80600
80910
|
}
|
|
80601
80911
|
});
|
|
80602
|
-
server.tool("browser_skill_run", "Run a pre-built browser skill (login, extract-pricing,
|
|
80912
|
+
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 }) => {
|
|
80603
80913
|
try {
|
|
80604
80914
|
const sid = resolveSessionId(session_id);
|
|
80605
80915
|
const page = getSessionPage(sid);
|
|
@@ -80617,7 +80927,7 @@ function register10(server) {
|
|
|
80617
80927
|
return err(e);
|
|
80618
80928
|
}
|
|
80619
80929
|
});
|
|
80620
|
-
server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot.
|
|
80930
|
+
server.tool("browser_batch", "Execute multiple browser actions in one call. Returns final snapshot.", {
|
|
80621
80931
|
session_id: exports_external2.string().optional(),
|
|
80622
80932
|
actions: exports_external2.array(exports_external2.object({
|
|
80623
80933
|
tool: exports_external2.string(),
|
|
@@ -80656,8 +80966,8 @@ function register10(server) {
|
|
|
80656
80966
|
break;
|
|
80657
80967
|
case "fill_form":
|
|
80658
80968
|
if (args.fields) {
|
|
80659
|
-
const { fillForm:
|
|
80660
|
-
const r = await
|
|
80969
|
+
const { fillForm: fillForm2 } = await Promise.resolve().then(() => (init_actions(), exports_actions));
|
|
80970
|
+
const r = await fillForm2(page, args.fields);
|
|
80661
80971
|
results.push({ tool: action.tool, success: true, result: r });
|
|
80662
80972
|
}
|
|
80663
80973
|
break;
|
|
@@ -80707,13 +81017,13 @@ function register10(server) {
|
|
|
80707
81017
|
return err(e);
|
|
80708
81018
|
}
|
|
80709
81019
|
});
|
|
80710
|
-
server.tool("browser_parallel", "Execute actions across multiple sessions in parallel.
|
|
81020
|
+
server.tool("browser_parallel", "Execute actions across multiple sessions in parallel.", {
|
|
80711
81021
|
actions: exports_external2.array(exports_external2.object({
|
|
80712
|
-
session_id: exports_external2.string()
|
|
80713
|
-
tool: exports_external2.string()
|
|
81022
|
+
session_id: exports_external2.string(),
|
|
81023
|
+
tool: exports_external2.string(),
|
|
80714
81024
|
args: exports_external2.record(exports_external2.unknown()).optional().default({})
|
|
80715
81025
|
})),
|
|
80716
|
-
timeout: exports_external2.number().optional().default(30000)
|
|
81026
|
+
timeout: exports_external2.number().optional().default(30000)
|
|
80717
81027
|
}, async ({ actions, timeout }) => {
|
|
80718
81028
|
try {
|
|
80719
81029
|
const t0 = Date.now();
|
|
@@ -80775,22 +81085,21 @@ function register10(server) {
|
|
|
80775
81085
|
}
|
|
80776
81086
|
});
|
|
80777
81087
|
const results = await Promise.all(promises);
|
|
80778
|
-
const duration_ms = Date.now() - t0;
|
|
80779
81088
|
const succeeded = results.filter((r) => r.success).length;
|
|
80780
81089
|
const failed = results.filter((r) => !r.success).length;
|
|
80781
|
-
return json({ results, duration_ms, succeeded, failed, total: actions.length });
|
|
81090
|
+
return json({ results, duration_ms: Date.now() - t0, succeeded, failed, total: actions.length });
|
|
80782
81091
|
} catch (e) {
|
|
80783
81092
|
return err(e);
|
|
80784
81093
|
}
|
|
80785
81094
|
});
|
|
80786
81095
|
server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
|
|
80787
81096
|
try {
|
|
80788
|
-
return json({ message: "Session pool not yet implemented in this version.
|
|
81097
|
+
return json({ message: "Session pool not yet implemented in this version.", ready: 0, total: 0 });
|
|
80789
81098
|
} catch (e) {
|
|
80790
81099
|
return err(e);
|
|
80791
81100
|
}
|
|
80792
81101
|
});
|
|
80793
|
-
server.tool("browser_cron_create", "Schedule a browser task to run automatically.
|
|
81102
|
+
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 }) => {
|
|
80794
81103
|
try {
|
|
80795
81104
|
const { createCronJob: createCronJob2 } = await Promise.resolve().then(() => (init_cron_manager(), exports_cron_manager));
|
|
80796
81105
|
return json(createCronJob2(schedule, { url, skill, extract: extract2 }, name));
|
|
@@ -80830,7 +81139,7 @@ function register10(server) {
|
|
|
80830
81139
|
return err(e);
|
|
80831
81140
|
}
|
|
80832
81141
|
});
|
|
80833
|
-
server.tool("browser_watch_url", "Monitor a URL for content changes on a schedule.
|
|
81142
|
+
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 }) => {
|
|
80834
81143
|
try {
|
|
80835
81144
|
const { createWatchJob: createWatchJob2 } = await Promise.resolve().then(() => (init_url_watcher(), exports_url_watcher));
|
|
80836
81145
|
return json(createWatchJob2(url, schedule, { name, selector }));
|
|
@@ -80862,7 +81171,7 @@ function register10(server) {
|
|
|
80862
81171
|
return err(e);
|
|
80863
81172
|
}
|
|
80864
81173
|
});
|
|
80865
|
-
server.tool("browser_task", "Execute a natural language browser task autonomously using Claude Haiku.
|
|
81174
|
+
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 }) => {
|
|
80866
81175
|
try {
|
|
80867
81176
|
const sid = resolveSessionId(session_id);
|
|
80868
81177
|
const page = getSessionPage(sid);
|
|
@@ -80873,10 +81182,22 @@ function register10(server) {
|
|
|
80873
81182
|
}
|
|
80874
81183
|
});
|
|
80875
81184
|
}
|
|
80876
|
-
var
|
|
81185
|
+
var init_integration = __esm(() => {
|
|
80877
81186
|
init_helpers();
|
|
80878
81187
|
});
|
|
80879
81188
|
|
|
81189
|
+
// src/mcp/meta.ts
|
|
81190
|
+
function register10(server) {
|
|
81191
|
+
registerAgentsAndProjects(server);
|
|
81192
|
+
registerGalleryAndDownloads(server);
|
|
81193
|
+
registerIntegrationAndMeta(server);
|
|
81194
|
+
}
|
|
81195
|
+
var init_meta = __esm(() => {
|
|
81196
|
+
init_agents3();
|
|
81197
|
+
init_gallery2();
|
|
81198
|
+
init_integration();
|
|
81199
|
+
});
|
|
81200
|
+
|
|
80880
81201
|
// src/mcp/data.ts
|
|
80881
81202
|
function register11(server) {
|
|
80882
81203
|
register8(server);
|
|
@@ -80891,32 +81212,74 @@ var init_data = __esm(() => {
|
|
|
80891
81212
|
|
|
80892
81213
|
// src/mcp/tui.ts
|
|
80893
81214
|
function assertTuiSession(sessionId) {
|
|
80894
|
-
const
|
|
80895
|
-
const engine = getSessionEngine2(sessionId);
|
|
81215
|
+
const engine = getSessionEngine(sessionId);
|
|
80896
81216
|
if (engine !== "tui") {
|
|
80897
|
-
throw new Error(`browser_tui_* tools require a TUI session (engine="tui"), but
|
|
81217
|
+
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")`);
|
|
80898
81218
|
}
|
|
80899
81219
|
}
|
|
80900
|
-
|
|
80901
|
-
|
|
80902
|
-
|
|
80903
|
-
|
|
80904
|
-
|
|
80905
|
-
|
|
80906
|
-
|
|
80907
|
-
|
|
80908
|
-
|
|
80909
|
-
|
|
80910
|
-
|
|
80911
|
-
|
|
80912
|
-
|
|
80913
|
-
|
|
80914
|
-
|
|
80915
|
-
|
|
80916
|
-
|
|
80917
|
-
|
|
80918
|
-
|
|
80919
|
-
|
|
81220
|
+
function getTuiSession(sessionId) {
|
|
81221
|
+
return getSessionTuiSession(sessionId);
|
|
81222
|
+
}
|
|
81223
|
+
function getTuiMeta(sessionId) {
|
|
81224
|
+
const session = getTuiSession(sessionId);
|
|
81225
|
+
return {
|
|
81226
|
+
method: session.method,
|
|
81227
|
+
reconnected: session.reconnectCount > 0
|
|
81228
|
+
};
|
|
81229
|
+
}
|
|
81230
|
+
function withMeta(sessionId, data) {
|
|
81231
|
+
return { ...data, ...getTuiMeta(sessionId) };
|
|
81232
|
+
}
|
|
81233
|
+
function withStableMeta(sessionId, data) {
|
|
81234
|
+
return { ...data, stuck: false, ...getTuiMeta(sessionId) };
|
|
81235
|
+
}
|
|
81236
|
+
function filterRows(rows, startRow, endRow) {
|
|
81237
|
+
const start = startRow ?? 0;
|
|
81238
|
+
const end = endRow ?? rows.length;
|
|
81239
|
+
const filtered = rows.slice(start, end);
|
|
81240
|
+
return {
|
|
81241
|
+
text: filtered.join(`
|
|
81242
|
+
`).trimEnd(),
|
|
81243
|
+
rows: filtered
|
|
81244
|
+
};
|
|
81245
|
+
}
|
|
81246
|
+
async function withTuiHealth(sessionId, operation, options = {}) {
|
|
81247
|
+
const {
|
|
81248
|
+
timeoutMs = DEFAULT_TOOL_TIMEOUT_MS2,
|
|
81249
|
+
reconnectOnStuck = RECONNECT_ON_STUCK,
|
|
81250
|
+
operationName = "operation"
|
|
81251
|
+
} = options;
|
|
81252
|
+
let session = getTuiSession(sessionId);
|
|
81253
|
+
let page = getSessionPage(sessionId);
|
|
81254
|
+
const health = await isTuiHealthy(session);
|
|
81255
|
+
if (!health.healthy && reconnectOnStuck && session.reconnectCount < 2) {
|
|
81256
|
+
try {
|
|
81257
|
+
const { getSessionCommand: getSessionCommand2, setSessionTui: setSessionTui2 } = await Promise.resolve().then(() => (init_session(), exports_session));
|
|
81258
|
+
const cmd = getSessionCommand2?.(sessionId) ?? "bash";
|
|
81259
|
+
const newSession = await reconnectTui(session, cmd, { method: session.method });
|
|
81260
|
+
setSessionTui2(sessionId, newSession);
|
|
81261
|
+
session = newSession;
|
|
81262
|
+
page = newSession.page;
|
|
81263
|
+
} catch {}
|
|
81264
|
+
} else if (!health.healthy) {
|
|
81265
|
+
throw Object.assign(new Error(`TUI session is unhealthy: ${health.reason}. Close and reopen the session.`), { code: "TUI_UNHEALTHY" });
|
|
81266
|
+
}
|
|
81267
|
+
let timedOut = false;
|
|
81268
|
+
const timer = setTimeout(() => {
|
|
81269
|
+
timedOut = true;
|
|
81270
|
+
}, timeoutMs);
|
|
81271
|
+
try {
|
|
81272
|
+
return await operation(page, session);
|
|
81273
|
+
} catch (error) {
|
|
81274
|
+
if (timedOut) {
|
|
81275
|
+
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.`);
|
|
81276
|
+
Object.assign(err2, { code: "TUI_TIMEOUT" });
|
|
81277
|
+
throw err2;
|
|
81278
|
+
}
|
|
81279
|
+
throw error;
|
|
81280
|
+
} finally {
|
|
81281
|
+
clearTimeout(timer);
|
|
81282
|
+
}
|
|
80920
81283
|
}
|
|
80921
81284
|
function register12(server) {
|
|
80922
81285
|
server.tool("browser_tui_send_keys", `Send keystrokes to a TUI terminal session. Use friendly key names.
|
|
@@ -80931,103 +81294,123 @@ SUPPORTED KEYS:
|
|
|
80931
81294
|
Pass multiple keys as a comma-separated string: "tab,tab,enter" or "ctrl+c"
|
|
80932
81295
|
For typing text, use browser_tui_send_text instead.`, {
|
|
80933
81296
|
session_id: exports_external2.string().optional(),
|
|
80934
|
-
keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'")
|
|
80935
|
-
|
|
81297
|
+
keys: exports_external2.string().describe("Comma-separated key names: 'enter', 'ctrl+c', 'tab,tab,enter', 'arrow_down,arrow_down,enter'"),
|
|
81298
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81299
|
+
}, async ({ session_id, keys, timeout_ms }) => {
|
|
80936
81300
|
try {
|
|
80937
81301
|
const sid = resolveSessionId(session_id);
|
|
80938
81302
|
assertTuiSession(sid);
|
|
80939
|
-
const
|
|
80940
|
-
|
|
80941
|
-
|
|
80942
|
-
|
|
80943
|
-
|
|
80944
|
-
|
|
80945
|
-
|
|
80946
|
-
|
|
81303
|
+
const result = await withTuiHealth(sid, async (page) => {
|
|
81304
|
+
const keyList = keys.split(",").map((k) => k.trim().toLowerCase());
|
|
81305
|
+
const sent = [];
|
|
81306
|
+
for (const key of keyList) {
|
|
81307
|
+
const mapped = KEY_MAP[key];
|
|
81308
|
+
if (mapped) {
|
|
81309
|
+
if (mapped.length === 1 && mapped.charCodeAt(0) < 32) {
|
|
81310
|
+
await page.keyboard.insertText(mapped);
|
|
81311
|
+
} else {
|
|
81312
|
+
await page.keyboard.press(mapped);
|
|
81313
|
+
}
|
|
81314
|
+
sent.push(key);
|
|
80947
81315
|
} else {
|
|
80948
|
-
await page.keyboard.press(
|
|
81316
|
+
await page.keyboard.press(key);
|
|
81317
|
+
sent.push(key);
|
|
80949
81318
|
}
|
|
80950
|
-
sent.push(key);
|
|
80951
|
-
} else {
|
|
80952
|
-
await page.keyboard.press(key);
|
|
80953
|
-
sent.push(key);
|
|
80954
81319
|
}
|
|
80955
|
-
|
|
80956
|
-
|
|
81320
|
+
return { sent, count: sent.length };
|
|
81321
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_send_keys" });
|
|
81322
|
+
return json(withStableMeta(sid, result));
|
|
80957
81323
|
} catch (e) {
|
|
81324
|
+
if (e.code === "TUI_TIMEOUT")
|
|
81325
|
+
return err(e);
|
|
81326
|
+
if (e.code === "TUI_UNHEALTHY")
|
|
81327
|
+
return err(e);
|
|
80958
81328
|
return err(e);
|
|
80959
81329
|
}
|
|
80960
81330
|
});
|
|
80961
|
-
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
|
|
80962
|
-
|
|
80963
|
-
Examples:
|
|
80964
|
-
- Send a command: text="ls -la", press_enter=true
|
|
80965
|
-
- Type without executing: text="partial input", press_enter=false
|
|
80966
|
-
- Send to a prompt: text="yes", press_enter=true`, {
|
|
81331
|
+
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.`, {
|
|
80967
81332
|
session_id: exports_external2.string().optional(),
|
|
80968
81333
|
text: exports_external2.string().describe("Text to type into the terminal"),
|
|
80969
|
-
press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)")
|
|
80970
|
-
|
|
81334
|
+
press_enter: exports_external2.boolean().optional().default(true).describe("Press Enter after typing (default: true)"),
|
|
81335
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81336
|
+
}, async ({ session_id, text, press_enter, timeout_ms }) => {
|
|
80971
81337
|
try {
|
|
80972
81338
|
const sid = resolveSessionId(session_id);
|
|
80973
81339
|
assertTuiSession(sid);
|
|
80974
|
-
const
|
|
80975
|
-
|
|
80976
|
-
|
|
80977
|
-
|
|
80978
|
-
|
|
80979
|
-
|
|
80980
|
-
|
|
80981
|
-
|
|
80982
|
-
|
|
80983
|
-
|
|
80984
|
-
|
|
81340
|
+
const result = await withTuiHealth(sid, async (page) => {
|
|
81341
|
+
const textarea = await page.$(".xterm-helper-textarea");
|
|
81342
|
+
if (textarea) {
|
|
81343
|
+
await textarea.type(text);
|
|
81344
|
+
} else {
|
|
81345
|
+
await page.keyboard.type(text);
|
|
81346
|
+
}
|
|
81347
|
+
if (press_enter)
|
|
81348
|
+
await page.keyboard.press("Enter");
|
|
81349
|
+
return { typed: text, pressed_enter: press_enter };
|
|
81350
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_send_text" });
|
|
81351
|
+
return json(withStableMeta(sid, result));
|
|
80985
81352
|
} catch (e) {
|
|
81353
|
+
if (e.code === "TUI_TIMEOUT")
|
|
81354
|
+
return err(e);
|
|
81355
|
+
if (e.code === "TUI_UNHEALTHY")
|
|
81356
|
+
return err(e);
|
|
80986
81357
|
return err(e);
|
|
80987
81358
|
}
|
|
80988
81359
|
});
|
|
80989
|
-
server.tool("browser_tui_resize", "Resize the terminal to a specific number of columns and rows.
|
|
81360
|
+
server.tool("browser_tui_resize", "Resize the terminal to a specific number of columns and rows.", {
|
|
80990
81361
|
session_id: exports_external2.string().optional(),
|
|
80991
81362
|
cols: exports_external2.number().describe("Number of columns (e.g. 80, 120, 200)"),
|
|
80992
|
-
rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)")
|
|
80993
|
-
|
|
81363
|
+
rows: exports_external2.number().describe("Number of rows (e.g. 24, 40, 50)"),
|
|
81364
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81365
|
+
}, async ({ session_id, cols, rows, timeout_ms }) => {
|
|
80994
81366
|
try {
|
|
80995
81367
|
const sid = resolveSessionId(session_id);
|
|
80996
81368
|
assertTuiSession(sid);
|
|
80997
|
-
const
|
|
80998
|
-
|
|
80999
|
-
|
|
81000
|
-
|
|
81001
|
-
|
|
81002
|
-
|
|
81003
|
-
|
|
81004
|
-
|
|
81005
|
-
|
|
81006
|
-
|
|
81369
|
+
const result = await withTuiHealth(sid, async (page) => {
|
|
81370
|
+
return page.evaluate((args) => {
|
|
81371
|
+
const [c, r] = args;
|
|
81372
|
+
const term = window.term ?? window.terminal;
|
|
81373
|
+
if (!term)
|
|
81374
|
+
return { resized: false, error: "No terminal instance found" };
|
|
81375
|
+
term.resize(c, r);
|
|
81376
|
+
return { resized: true, cols: c, rows: r };
|
|
81377
|
+
}, [cols, rows]);
|
|
81378
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_resize" });
|
|
81379
|
+
return json(withMeta(sid, result));
|
|
81007
81380
|
} catch (e) {
|
|
81381
|
+
if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
|
|
81382
|
+
return err(e);
|
|
81008
81383
|
return err(e);
|
|
81009
81384
|
}
|
|
81010
81385
|
});
|
|
81011
|
-
server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range
|
|
81012
|
-
|
|
81013
|
-
Use this to read what the terminal is currently displaying. For waiting until specific text appears, use browser_tui_wait_for_text instead.`, {
|
|
81386
|
+
server.tool("browser_tui_get_text", `Get the text content from the terminal buffer. Returns all visible text, or a specific row range.`, {
|
|
81014
81387
|
session_id: exports_external2.string().optional(),
|
|
81015
81388
|
start_row: exports_external2.number().optional().describe("First row to read (0-indexed, default: 0)"),
|
|
81016
|
-
end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows.")
|
|
81017
|
-
|
|
81389
|
+
end_row: exports_external2.number().optional().describe("Last row (exclusive). Omit for all rows."),
|
|
81390
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81391
|
+
}, async ({ session_id, start_row, end_row, timeout_ms }) => {
|
|
81018
81392
|
try {
|
|
81019
81393
|
const sid = resolveSessionId(session_id);
|
|
81020
81394
|
assertTuiSession(sid);
|
|
81021
|
-
const
|
|
81022
|
-
|
|
81023
|
-
|
|
81395
|
+
const result = await withTuiHealth(sid, async (page, session) => {
|
|
81396
|
+
const state = await getTerminalState(page, session.method, timeout_ms);
|
|
81397
|
+
const filtered = filterRows(state.rows, start_row, end_row);
|
|
81398
|
+
return {
|
|
81399
|
+
...filtered,
|
|
81400
|
+
row_count: state.row_count
|
|
81401
|
+
};
|
|
81402
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_get_text" });
|
|
81403
|
+
return json(withMeta(sid, result));
|
|
81024
81404
|
} catch (e) {
|
|
81405
|
+
if (e.code === "TUI_TIMEOUT")
|
|
81406
|
+
return err(e);
|
|
81407
|
+
if (e.code === "TUI_UNHEALTHY")
|
|
81408
|
+
return err(e);
|
|
81025
81409
|
return err(e);
|
|
81026
81410
|
}
|
|
81027
81411
|
});
|
|
81028
|
-
server.tool("browser_tui_wait_for_text", `Wait for specific text to appear in the terminal output. Polls
|
|
81029
|
-
|
|
81030
|
-
Use this after sending a command to wait for its output, or to wait for a TUI app to finish loading.`, {
|
|
81412
|
+
server.tool("browser_tui_wait_for_text", `Wait for specific text to appear in the terminal output. Polls until found or timeout.
|
|
81413
|
+
Returns stuck:true if the terminal became unresponsive during the wait.`, {
|
|
81031
81414
|
session_id: exports_external2.string().optional(),
|
|
81032
81415
|
text: exports_external2.string().describe("Text to wait for (substring match)"),
|
|
81033
81416
|
timeout_ms: exports_external2.number().optional().default(30000).describe("Timeout in milliseconds (default: 30000)")
|
|
@@ -81035,38 +81418,37 @@ Use this after sending a command to wait for its output, or to wait for a TUI ap
|
|
|
81035
81418
|
try {
|
|
81036
81419
|
const sid = resolveSessionId(session_id);
|
|
81037
81420
|
assertTuiSession(sid);
|
|
81038
|
-
const
|
|
81039
|
-
|
|
81040
|
-
|
|
81041
|
-
|
|
81042
|
-
if (result.text.includes(text)) {
|
|
81043
|
-
return json({ found: true, elapsed_ms: Date.now() - start, terminal_text: result.text });
|
|
81044
|
-
}
|
|
81045
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
81046
|
-
}
|
|
81047
|
-
const finalText = await getTermText(page);
|
|
81048
|
-
return json({ found: false, elapsed_ms: timeout_ms, terminal_text: finalText.text });
|
|
81421
|
+
const result = await withTuiHealth(sid, async (page, session) => {
|
|
81422
|
+
return waitForTerminalText(page, text, timeout_ms, session.method);
|
|
81423
|
+
}, { timeoutMs: timeout_ms + 5000, operationName: "browser_tui_wait_for_text" });
|
|
81424
|
+
return json(withMeta(sid, result));
|
|
81049
81425
|
} catch (e) {
|
|
81426
|
+
if (e.code === "TUI_TIMEOUT")
|
|
81427
|
+
return err(e);
|
|
81428
|
+
if (e.code === "TUI_UNHEALTHY")
|
|
81429
|
+
return err(e);
|
|
81050
81430
|
return err(e);
|
|
81051
81431
|
}
|
|
81052
81432
|
});
|
|
81053
81433
|
server.tool("browser_tui_get_cursor", "Get the current cursor position (row and column) in the terminal.", {
|
|
81054
|
-
session_id: exports_external2.string().optional()
|
|
81055
|
-
|
|
81434
|
+
session_id: exports_external2.string().optional(),
|
|
81435
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81436
|
+
}, async ({ session_id, timeout_ms }) => {
|
|
81056
81437
|
try {
|
|
81057
81438
|
const sid = resolveSessionId(session_id);
|
|
81058
81439
|
assertTuiSession(sid);
|
|
81059
|
-
const
|
|
81060
|
-
|
|
81061
|
-
|
|
81062
|
-
if (!term?.buffer?.active)
|
|
81440
|
+
const result = await withTuiHealth(sid, async (page, session) => {
|
|
81441
|
+
const state = await getTerminalState(page, session.method, timeout_ms);
|
|
81442
|
+
if (state.cursor_row < 0 || state.cursor_col < 0)
|
|
81063
81443
|
return null;
|
|
81064
|
-
return { row:
|
|
81065
|
-
});
|
|
81066
|
-
if (!
|
|
81067
|
-
return err(new Error("Could not read cursor
|
|
81068
|
-
return json(
|
|
81444
|
+
return { row: state.cursor_row, col: state.cursor_col };
|
|
81445
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_get_cursor" });
|
|
81446
|
+
if (!result)
|
|
81447
|
+
return err(new Error("Could not read cursor \u2014 no terminal instance"));
|
|
81448
|
+
return json(withStableMeta(sid, result));
|
|
81069
81449
|
} catch (e) {
|
|
81450
|
+
if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
|
|
81451
|
+
return err(e);
|
|
81070
81452
|
return err(e);
|
|
81071
81453
|
}
|
|
81072
81454
|
});
|
|
@@ -81077,140 +81459,135 @@ CONDITION SYNTAX:
|
|
|
81077
81459
|
- "row N contains X" \u2014 row N (0-indexed) contains substring X
|
|
81078
81460
|
- "cursor at R,C" \u2014 cursor is at row R, column C
|
|
81079
81461
|
- "row_count > N" \u2014 total rows greater than N
|
|
81080
|
-
- "row_count == N" \u2014 total rows equals N
|
|
81081
|
-
|
|
81082
|
-
Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
|
|
81462
|
+
- "row_count == N" \u2014 total rows equals N`, {
|
|
81083
81463
|
session_id: exports_external2.string().optional(),
|
|
81084
|
-
condition: exports_external2.string().describe("Assertion condition(s), joined with AND")
|
|
81085
|
-
|
|
81464
|
+
condition: exports_external2.string().describe("Assertion condition(s), joined with AND"),
|
|
81465
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81466
|
+
}, async ({ session_id, condition, timeout_ms }) => {
|
|
81086
81467
|
try {
|
|
81087
81468
|
const sid = resolveSessionId(session_id);
|
|
81088
81469
|
assertTuiSession(sid);
|
|
81089
|
-
const
|
|
81090
|
-
|
|
81091
|
-
|
|
81092
|
-
const
|
|
81093
|
-
|
|
81094
|
-
|
|
81095
|
-
|
|
81096
|
-
|
|
81097
|
-
|
|
81098
|
-
|
|
81099
|
-
|
|
81100
|
-
|
|
81101
|
-
|
|
81102
|
-
|
|
81103
|
-
|
|
81104
|
-
|
|
81105
|
-
|
|
81106
|
-
|
|
81107
|
-
|
|
81108
|
-
|
|
81109
|
-
const
|
|
81110
|
-
|
|
81111
|
-
|
|
81112
|
-
|
|
81113
|
-
|
|
81114
|
-
|
|
81115
|
-
|
|
81116
|
-
|
|
81117
|
-
|
|
81118
|
-
|
|
81119
|
-
|
|
81120
|
-
|
|
81121
|
-
|
|
81122
|
-
|
|
81123
|
-
|
|
81124
|
-
|
|
81125
|
-
}
|
|
81126
|
-
|
|
81127
|
-
|
|
81128
|
-
allPassed = false;
|
|
81129
|
-
}
|
|
81130
|
-
return json({ passed: allPassed, checks, cursor, row_count: termData.row_count });
|
|
81470
|
+
const result = await withTuiHealth(sid, async (page, session) => {
|
|
81471
|
+
const state = await getTerminalState(page, session.method, timeout_ms);
|
|
81472
|
+
const termText = state.text;
|
|
81473
|
+
const cursor = { row: state.cursor_row, col: state.cursor_col };
|
|
81474
|
+
const checks = [];
|
|
81475
|
+
let allPassed = true;
|
|
81476
|
+
for (const part of condition.split(/\s+AND\s+/i)) {
|
|
81477
|
+
const trimmed = part.trim();
|
|
81478
|
+
let passed = false;
|
|
81479
|
+
if (/^text\s+contains\s+/i.test(trimmed)) {
|
|
81480
|
+
const needle = trimmed.replace(/^text\s+contains\s+/i, "").replace(/^["']|["']$/g, "");
|
|
81481
|
+
passed = termText.includes(needle);
|
|
81482
|
+
} else if (/^row\s+(\d+)\s+contains\s+/i.test(trimmed)) {
|
|
81483
|
+
const match = trimmed.match(/^row\s+(\d+)\s+contains\s+(.+)/i);
|
|
81484
|
+
if (match) {
|
|
81485
|
+
const rowIdx = parseInt(match[1]);
|
|
81486
|
+
const needle = match[2].replace(/^["']|["']$/g, "");
|
|
81487
|
+
passed = (state.rows[rowIdx] ?? "").includes(needle);
|
|
81488
|
+
}
|
|
81489
|
+
} else if (/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i.test(trimmed)) {
|
|
81490
|
+
const match = trimmed.match(/^cursor\s+at\s+(\d+)\s*,\s*(\d+)/i);
|
|
81491
|
+
if (match)
|
|
81492
|
+
passed = cursor.row === parseInt(match[1]) && cursor.col === parseInt(match[2]);
|
|
81493
|
+
} else if (/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i.test(trimmed)) {
|
|
81494
|
+
const match = trimmed.match(/^row_count\s*(>|>=|<|<=|==|!=)\s*(\d+)/i);
|
|
81495
|
+
if (match) {
|
|
81496
|
+
const op = match[1];
|
|
81497
|
+
const n = parseInt(match[2]);
|
|
81498
|
+
const cnt = state.row_count;
|
|
81499
|
+
passed = op === ">" ? cnt > n : op === ">=" ? cnt >= n : op === "<" ? cnt < n : op === "<=" ? cnt <= n : op === "==" ? cnt === n : cnt !== n;
|
|
81500
|
+
}
|
|
81501
|
+
}
|
|
81502
|
+
checks.push({ assertion: trimmed, result: passed });
|
|
81503
|
+
if (!passed)
|
|
81504
|
+
allPassed = false;
|
|
81505
|
+
}
|
|
81506
|
+
return { passed: allPassed, checks, cursor, row_count: state.row_count };
|
|
81507
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_assert" });
|
|
81508
|
+
return json(withMeta(sid, result));
|
|
81131
81509
|
} catch (e) {
|
|
81510
|
+
if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
|
|
81511
|
+
return err(e);
|
|
81132
81512
|
return err(e);
|
|
81133
81513
|
}
|
|
81134
81514
|
});
|
|
81135
|
-
server.tool("browser_tui_snapshot", "Capture a structured snapshot of the terminal buffer: all rows
|
|
81136
|
-
session_id: exports_external2.string().optional()
|
|
81137
|
-
|
|
81515
|
+
server.tool("browser_tui_snapshot", "Capture a structured snapshot of the terminal buffer: all rows, row refs, cursor position, dimensions, and theme.", {
|
|
81516
|
+
session_id: exports_external2.string().optional(),
|
|
81517
|
+
timeout_ms: exports_external2.number().optional().default(15000).describe("Hard timeout in ms (default: 15000)")
|
|
81518
|
+
}, async ({ session_id, timeout_ms }) => {
|
|
81138
81519
|
try {
|
|
81139
81520
|
const sid = resolveSessionId(session_id);
|
|
81140
81521
|
assertTuiSession(sid);
|
|
81141
|
-
const
|
|
81142
|
-
|
|
81143
|
-
const term = window.term ?? window.terminal;
|
|
81144
|
-
if (!term?.buffer?.active)
|
|
81145
|
-
return null;
|
|
81146
|
-
const buf = term.buffer.active;
|
|
81147
|
-
const rows = [];
|
|
81148
|
-
for (let i = 0;i < buf.length; i++) {
|
|
81149
|
-
const line = buf.getLine(i);
|
|
81150
|
-
if (line)
|
|
81151
|
-
rows.push(line.translateToString(true));
|
|
81152
|
-
}
|
|
81522
|
+
const result = await withTuiHealth(sid, async (page, session) => {
|
|
81523
|
+
const state = await getTerminalState(page, session.method, timeout_ms);
|
|
81153
81524
|
return {
|
|
81154
|
-
rows,
|
|
81155
|
-
|
|
81156
|
-
|
|
81157
|
-
|
|
81158
|
-
|
|
81159
|
-
|
|
81160
|
-
|
|
81161
|
-
|
|
81525
|
+
rows: state.rows,
|
|
81526
|
+
refs: state.refs,
|
|
81527
|
+
cols: state.cols,
|
|
81528
|
+
total_rows: state.total_rows,
|
|
81529
|
+
buffer_length: state.buffer_length,
|
|
81530
|
+
cursor_row: state.cursor_row,
|
|
81531
|
+
cursor_col: state.cursor_col,
|
|
81532
|
+
font_size: state.font_size,
|
|
81533
|
+
theme: state.theme
|
|
81162
81534
|
};
|
|
81163
|
-
});
|
|
81164
|
-
|
|
81165
|
-
return err(new Error("Could not capture snapshot \u2014 no terminal instance"));
|
|
81166
|
-
return json(snapshot);
|
|
81535
|
+
}, { timeoutMs: timeout_ms, operationName: "browser_tui_snapshot" });
|
|
81536
|
+
return json(withStableMeta(sid, result));
|
|
81167
81537
|
} catch (e) {
|
|
81538
|
+
if (e.code === "TUI_TIMEOUT" || e.code === "TUI_UNHEALTHY")
|
|
81539
|
+
return err(e);
|
|
81168
81540
|
return err(e);
|
|
81169
81541
|
}
|
|
81170
81542
|
});
|
|
81171
|
-
server.tool("browser_tui_record_start", "Start recording the terminal session as an asciicast v2 file
|
|
81543
|
+
server.tool("browser_tui_record_start", "Start recording the terminal session as an asciicast v2 file.", {
|
|
81172
81544
|
session_id: exports_external2.string().optional(),
|
|
81173
81545
|
interval_ms: exports_external2.number().optional().default(500).describe("Polling interval in ms (default: 500)")
|
|
81174
81546
|
}, async ({ session_id, interval_ms }) => {
|
|
81175
81547
|
try {
|
|
81176
81548
|
const sid = resolveSessionId(session_id);
|
|
81177
81549
|
assertTuiSession(sid);
|
|
81178
|
-
const page = getSessionPage(sid);
|
|
81179
81550
|
if (activeRecordings2.has(sid)) {
|
|
81180
81551
|
return err(new Error("Recording already active for this session. Stop it first with browser_tui_record_stop."));
|
|
81181
81552
|
}
|
|
81182
|
-
const
|
|
81183
|
-
|
|
81184
|
-
|
|
81185
|
-
}
|
|
81186
|
-
const initialText = (await getTermText(page)).text;
|
|
81553
|
+
const page = getSessionPage(sid);
|
|
81554
|
+
const session = getTuiSession(sid);
|
|
81555
|
+
const initialState = await getTerminalState(page, session.method);
|
|
81556
|
+
const dims = { cols: initialState.cols ?? 80, rows: initialState.total_rows || initialState.row_count || 24 };
|
|
81187
81557
|
const recording = {
|
|
81188
81558
|
sessionId: sid,
|
|
81189
81559
|
startTime: Date.now(),
|
|
81190
81560
|
cols: dims.cols,
|
|
81191
81561
|
rows: dims.rows,
|
|
81192
81562
|
events: [],
|
|
81193
|
-
lastText:
|
|
81563
|
+
lastText: initialState.text,
|
|
81194
81564
|
intervalId: setInterval(async () => {
|
|
81195
81565
|
try {
|
|
81196
|
-
const
|
|
81197
|
-
|
|
81566
|
+
const currentPage = getSessionPage(sid);
|
|
81567
|
+
const currentSession = getTuiSession(sid);
|
|
81568
|
+
const state = await getTerminalState(currentPage, currentSession.method);
|
|
81569
|
+
if (state.text !== recording.lastText) {
|
|
81198
81570
|
const elapsed = (Date.now() - recording.startTime) / 1000;
|
|
81199
|
-
recording.events.push([elapsed, "o",
|
|
81200
|
-
recording.lastText =
|
|
81571
|
+
recording.events.push([elapsed, "o", state.text.slice(recording.lastText.length) || state.text]);
|
|
81572
|
+
recording.lastText = state.text;
|
|
81201
81573
|
}
|
|
81202
81574
|
} catch {}
|
|
81203
81575
|
}, interval_ms)
|
|
81204
81576
|
};
|
|
81205
81577
|
activeRecordings2.set(sid, recording);
|
|
81206
|
-
return json({
|
|
81578
|
+
return json({
|
|
81579
|
+
recording: true,
|
|
81580
|
+
session_id: sid,
|
|
81581
|
+
interval_ms,
|
|
81582
|
+
cols: dims.cols,
|
|
81583
|
+
rows: dims.rows,
|
|
81584
|
+
method: session.method
|
|
81585
|
+
});
|
|
81207
81586
|
} catch (e) {
|
|
81208
81587
|
return err(e);
|
|
81209
81588
|
}
|
|
81210
81589
|
});
|
|
81211
|
-
server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON.
|
|
81212
|
-
session_id: exports_external2.string().optional()
|
|
81213
|
-
}, async ({ session_id }) => {
|
|
81590
|
+
server.tool("browser_tui_record_stop", "Stop recording and return the asciicast v2 JSON.", { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
|
|
81214
81591
|
try {
|
|
81215
81592
|
const sid = resolveSessionId(session_id);
|
|
81216
81593
|
const recording = activeRecordings2.get(sid);
|
|
@@ -81228,25 +81605,45 @@ Example: "text contains hello AND row 0 contains $ AND cursor at 1,0"`, {
|
|
|
81228
81605
|
env: { TERM: "xterm-256color", SHELL: "/bin/bash" }
|
|
81229
81606
|
};
|
|
81230
81607
|
const lines = [JSON.stringify(header)];
|
|
81231
|
-
for (const [time, type2, data] of recording.events)
|
|
81608
|
+
for (const [time, type2, data] of recording.events)
|
|
81232
81609
|
lines.push(JSON.stringify([time, type2, data]));
|
|
81233
|
-
}
|
|
81234
81610
|
const asciicast = lines.join(`
|
|
81235
81611
|
`);
|
|
81236
81612
|
return json({
|
|
81237
81613
|
format: "asciicast_v2",
|
|
81238
81614
|
duration_seconds: Math.round(duration * 10) / 10,
|
|
81239
81615
|
event_count: recording.events.length,
|
|
81240
|
-
asciicast
|
|
81616
|
+
asciicast,
|
|
81617
|
+
method: getTuiSession(sid).method
|
|
81618
|
+
});
|
|
81619
|
+
} catch (e) {
|
|
81620
|
+
return err(e);
|
|
81621
|
+
}
|
|
81622
|
+
});
|
|
81623
|
+
server.tool("browser_tui_health", `Health check for a TUI session. Returns healthy status, latency, reconnect count, and the active read method.
|
|
81624
|
+
Use this to verify a session is still responsive before running other tools.`, { session_id: exports_external2.string().optional() }, async ({ session_id }) => {
|
|
81625
|
+
try {
|
|
81626
|
+
const sid = resolveSessionId(session_id);
|
|
81627
|
+
assertTuiSession(sid);
|
|
81628
|
+
const session = getTuiSession(sid);
|
|
81629
|
+
const health = await isTuiHealthy(session);
|
|
81630
|
+
return json({
|
|
81631
|
+
healthy: health.healthy,
|
|
81632
|
+
latency_ms: health.healthy ? health.latency_ms : null,
|
|
81633
|
+
reason: health.healthy ? null : health.reason,
|
|
81634
|
+
reconnect_count: session.reconnectCount,
|
|
81635
|
+
method: session.method
|
|
81241
81636
|
});
|
|
81242
81637
|
} catch (e) {
|
|
81243
81638
|
return err(e);
|
|
81244
81639
|
}
|
|
81245
81640
|
});
|
|
81246
81641
|
}
|
|
81247
|
-
var KEY_MAP, activeRecordings2;
|
|
81642
|
+
var DEFAULT_TOOL_TIMEOUT_MS2 = 15000, RECONNECT_ON_STUCK = true, KEY_MAP, activeRecordings2;
|
|
81248
81643
|
var init_tui2 = __esm(() => {
|
|
81249
81644
|
init_helpers();
|
|
81645
|
+
init_tui();
|
|
81646
|
+
init_session();
|
|
81250
81647
|
KEY_MAP = {
|
|
81251
81648
|
"ctrl+c": "\x03",
|
|
81252
81649
|
"ctrl+d": "\x04",
|
|
@@ -81368,32 +81765,71 @@ var init_snapshots = __esm(() => {
|
|
|
81368
81765
|
var exports_server = {};
|
|
81369
81766
|
import { join as join28 } from "path";
|
|
81370
81767
|
import { existsSync as existsSync19 } from "fs";
|
|
81371
|
-
function
|
|
81768
|
+
function corsHeaders(origin) {
|
|
81769
|
+
const headers = {
|
|
81770
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
81771
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
81772
|
+
};
|
|
81773
|
+
if (origin) {
|
|
81774
|
+
if (!API_KEY && !origin.startsWith("http://localhost") && !origin.startsWith("http://127.0.0.1")) {
|
|
81775
|
+
headers["Access-Control-Allow-Origin"] = ALLOWED_ORIGIN ?? "http://localhost:3000";
|
|
81776
|
+
} else {
|
|
81777
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
81778
|
+
}
|
|
81779
|
+
}
|
|
81780
|
+
return headers;
|
|
81781
|
+
}
|
|
81782
|
+
function authenticate(req) {
|
|
81783
|
+
if (!API_KEY)
|
|
81784
|
+
return null;
|
|
81785
|
+
const header = req.headers.get("Authorization") ?? "";
|
|
81786
|
+
const token = header.startsWith("Bearer ") ? header.slice(7) : "";
|
|
81787
|
+
if (token !== API_KEY) {
|
|
81788
|
+
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
81789
|
+
status: 401,
|
|
81790
|
+
headers: { "Content-Type": "application/json" }
|
|
81791
|
+
});
|
|
81792
|
+
}
|
|
81793
|
+
return null;
|
|
81794
|
+
}
|
|
81795
|
+
async function safeJson(req) {
|
|
81796
|
+
try {
|
|
81797
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
81798
|
+
if (!contentType.includes("application/json")) {
|
|
81799
|
+
return { error: badRequest("Content-Type must be application/json") };
|
|
81800
|
+
}
|
|
81801
|
+
const body = await req.json();
|
|
81802
|
+
return { body };
|
|
81803
|
+
} catch {
|
|
81804
|
+
return { error: badRequest("Invalid or missing JSON body") };
|
|
81805
|
+
}
|
|
81806
|
+
}
|
|
81807
|
+
function ok(data, status = 200, extraHeaders) {
|
|
81372
81808
|
return new Response(JSON.stringify(data), {
|
|
81373
81809
|
status,
|
|
81374
|
-
headers: { "Content-Type": "application/json", ...
|
|
81810
|
+
headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
|
|
81375
81811
|
});
|
|
81376
81812
|
}
|
|
81377
|
-
function notFound(msg) {
|
|
81813
|
+
function notFound(msg, extraHeaders) {
|
|
81378
81814
|
return new Response(JSON.stringify({ error: msg }), {
|
|
81379
81815
|
status: 404,
|
|
81380
|
-
headers: { "Content-Type": "application/json", ...
|
|
81816
|
+
headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
|
|
81381
81817
|
});
|
|
81382
81818
|
}
|
|
81383
|
-
function badRequest(msg) {
|
|
81819
|
+
function badRequest(msg, extraHeaders) {
|
|
81384
81820
|
return new Response(JSON.stringify({ error: msg }), {
|
|
81385
81821
|
status: 400,
|
|
81386
|
-
headers: { "Content-Type": "application/json", ...
|
|
81822
|
+
headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
|
|
81387
81823
|
});
|
|
81388
81824
|
}
|
|
81389
|
-
function serverError(e) {
|
|
81825
|
+
function serverError(e, extraHeaders) {
|
|
81390
81826
|
const msg = e instanceof Error ? e.message : String(e);
|
|
81391
81827
|
return new Response(JSON.stringify({ error: msg }), {
|
|
81392
81828
|
status: 500,
|
|
81393
|
-
headers: { "Content-Type": "application/json", ...
|
|
81829
|
+
headers: { "Content-Type": "application/json", ...extraHeaders ?? {} }
|
|
81394
81830
|
});
|
|
81395
81831
|
}
|
|
81396
|
-
var PORT,
|
|
81832
|
+
var PORT, API_KEY, ALLOWED_ORIGIN, startTime, networkCleanup, consoleCleanup, harCaptures2, server2;
|
|
81397
81833
|
var init_server = __esm(() => {
|
|
81398
81834
|
init_session();
|
|
81399
81835
|
init_actions();
|
|
@@ -81412,12 +81848,9 @@ var init_server = __esm(() => {
|
|
|
81412
81848
|
init_downloads();
|
|
81413
81849
|
init_gallery_diff();
|
|
81414
81850
|
PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
|
|
81851
|
+
API_KEY = process.env["BROWSER_API_KEY"] ?? null;
|
|
81852
|
+
ALLOWED_ORIGIN = process.env["BROWSER_ALLOWED_ORIGIN"] ?? (API_KEY ? null : "http://localhost:3000");
|
|
81415
81853
|
startTime = Date.now();
|
|
81416
|
-
CORS_HEADERS = {
|
|
81417
|
-
"Access-Control-Allow-Origin": "*",
|
|
81418
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
81419
|
-
"Access-Control-Allow-Headers": "Content-Type"
|
|
81420
|
-
};
|
|
81421
81854
|
networkCleanup = new Map;
|
|
81422
81855
|
consoleCleanup = new Map;
|
|
81423
81856
|
harCaptures2 = new Map;
|
|
@@ -81427,8 +81860,15 @@ var init_server = __esm(() => {
|
|
|
81427
81860
|
const url = new URL(req.url);
|
|
81428
81861
|
const path = url.pathname;
|
|
81429
81862
|
const method = req.method;
|
|
81863
|
+
const origin = req.headers.get("Origin") ?? undefined;
|
|
81864
|
+
const headers = corsHeaders(origin ?? null);
|
|
81430
81865
|
if (method === "OPTIONS") {
|
|
81431
|
-
return new Response(null, { status: 204, headers
|
|
81866
|
+
return new Response(null, { status: 204, headers });
|
|
81867
|
+
}
|
|
81868
|
+
if (!path.startsWith("/dashboard") && path !== "/health") {
|
|
81869
|
+
const authError = authenticate(req);
|
|
81870
|
+
if (authError)
|
|
81871
|
+
return authError;
|
|
81432
81872
|
}
|
|
81433
81873
|
try {
|
|
81434
81874
|
if (path === "/health" && method === "GET") {
|
|
@@ -81445,7 +81885,10 @@ var init_server = __esm(() => {
|
|
|
81445
81885
|
return ok({ sessions: listSessions2(status ? { status, projectId } : { projectId }) });
|
|
81446
81886
|
}
|
|
81447
81887
|
if (path === "/api/sessions" && method === "POST") {
|
|
81448
|
-
const
|
|
81888
|
+
const parsed = await safeJson(req);
|
|
81889
|
+
if ("error" in parsed)
|
|
81890
|
+
return parsed.error;
|
|
81891
|
+
const body = parsed.body;
|
|
81449
81892
|
const { session } = await createSession2({
|
|
81450
81893
|
engine: body.engine ?? "auto",
|
|
81451
81894
|
projectId: body.project_id,
|
|
@@ -81467,25 +81910,36 @@ var init_server = __esm(() => {
|
|
|
81467
81910
|
return ok({ session });
|
|
81468
81911
|
}
|
|
81469
81912
|
if (path === "/api/navigate" && method === "POST") {
|
|
81470
|
-
const
|
|
81913
|
+
const parsed = await safeJson(req);
|
|
81914
|
+
if ("error" in parsed)
|
|
81915
|
+
return parsed.error;
|
|
81916
|
+
const body = parsed.body;
|
|
81471
81917
|
if (!body.session_id || !body.url)
|
|
81472
|
-
return badRequest("session_id and url required");
|
|
81473
|
-
const
|
|
81474
|
-
|
|
81475
|
-
|
|
81918
|
+
return badRequest("session_id and url required", headers);
|
|
81919
|
+
const sessionId = body.session_id;
|
|
81920
|
+
const url2 = body.url;
|
|
81921
|
+
const page = getSessionPage(sessionId);
|
|
81922
|
+
await navigate(page, url2);
|
|
81923
|
+
return ok({ url: url2, title: await page.title(), current_url: page.url() });
|
|
81476
81924
|
}
|
|
81477
81925
|
if (path === "/api/extract" && method === "POST") {
|
|
81478
|
-
const
|
|
81926
|
+
const parsed = await safeJson(req);
|
|
81927
|
+
if ("error" in parsed)
|
|
81928
|
+
return parsed.error;
|
|
81929
|
+
const body = parsed.body;
|
|
81479
81930
|
if (!body.session_id)
|
|
81480
|
-
return badRequest("session_id required");
|
|
81931
|
+
return badRequest("session_id required", headers);
|
|
81481
81932
|
const page = getSessionPage(body.session_id);
|
|
81482
81933
|
const result = await extract(page, { format: body.format, selector: body.selector });
|
|
81483
81934
|
return ok(result);
|
|
81484
81935
|
}
|
|
81485
81936
|
if (path === "/api/screenshot" && method === "POST") {
|
|
81486
|
-
const
|
|
81937
|
+
const parsed = await safeJson(req);
|
|
81938
|
+
if ("error" in parsed)
|
|
81939
|
+
return parsed.error;
|
|
81940
|
+
const body = parsed.body;
|
|
81487
81941
|
if (!body.session_id)
|
|
81488
|
-
return badRequest("session_id required");
|
|
81942
|
+
return badRequest("session_id required", headers);
|
|
81489
81943
|
const page = getSessionPage(body.session_id);
|
|
81490
81944
|
const result = await takeScreenshot(page, { selector: body.selector, fullPage: body.full_page });
|
|
81491
81945
|
return ok(result);
|
|
@@ -81522,16 +81976,22 @@ var init_server = __esm(() => {
|
|
|
81522
81976
|
return ok({ metrics: await getPerformanceMetrics(page) });
|
|
81523
81977
|
}
|
|
81524
81978
|
if (path === "/api/har/start" && method === "POST") {
|
|
81525
|
-
const
|
|
81979
|
+
const parsed = await safeJson(req);
|
|
81980
|
+
if ("error" in parsed)
|
|
81981
|
+
return parsed.error;
|
|
81982
|
+
const body = parsed.body;
|
|
81526
81983
|
const page = getSessionPage(body.session_id);
|
|
81527
81984
|
harCaptures2.set(body.session_id, startHAR(page));
|
|
81528
81985
|
return ok({ started: true });
|
|
81529
81986
|
}
|
|
81530
81987
|
if (path === "/api/har/stop" && method === "POST") {
|
|
81531
|
-
const
|
|
81988
|
+
const parsed = await safeJson(req);
|
|
81989
|
+
if ("error" in parsed)
|
|
81990
|
+
return parsed.error;
|
|
81991
|
+
const body = parsed.body;
|
|
81532
81992
|
const capture = harCaptures2.get(body.session_id);
|
|
81533
81993
|
if (!capture)
|
|
81534
|
-
return notFound("No active HAR capture");
|
|
81994
|
+
return notFound("No active HAR capture", headers);
|
|
81535
81995
|
const har = capture.stop();
|
|
81536
81996
|
harCaptures2.delete(body.session_id);
|
|
81537
81997
|
return ok({ har });
|
|
@@ -81540,8 +82000,11 @@ var init_server = __esm(() => {
|
|
|
81540
82000
|
return ok({ recordings: listRecordings(url.searchParams.get("project_id") ?? undefined) });
|
|
81541
82001
|
}
|
|
81542
82002
|
if (path.match(/^\/api\/recordings\/([^/]+)\/replay$/) && method === "POST") {
|
|
82003
|
+
const parsed = await safeJson(req);
|
|
82004
|
+
if ("error" in parsed)
|
|
82005
|
+
return parsed.error;
|
|
82006
|
+
const body = parsed.body;
|
|
81543
82007
|
const id = path.split("/")[3];
|
|
81544
|
-
const body = await req.json();
|
|
81545
82008
|
const page = getSessionPage(body.session_id);
|
|
81546
82009
|
const result = await replayRecording(id, page);
|
|
81547
82010
|
return ok(result);
|
|
@@ -81553,9 +82016,12 @@ var init_server = __esm(() => {
|
|
|
81553
82016
|
return ok({ deleted: id });
|
|
81554
82017
|
}
|
|
81555
82018
|
if (path === "/api/crawl" && method === "POST") {
|
|
81556
|
-
const
|
|
82019
|
+
const parsed = await safeJson(req);
|
|
82020
|
+
if ("error" in parsed)
|
|
82021
|
+
return parsed.error;
|
|
82022
|
+
const body = parsed.body;
|
|
81557
82023
|
if (!body.url)
|
|
81558
|
-
return badRequest("url required");
|
|
82024
|
+
return badRequest("url required", headers);
|
|
81559
82025
|
const result = await crawl(body.url, {
|
|
81560
82026
|
maxDepth: body.max_depth ?? 2,
|
|
81561
82027
|
maxPages: body.max_pages ?? 50,
|
|
@@ -81567,9 +82033,12 @@ var init_server = __esm(() => {
|
|
|
81567
82033
|
return ok({ agents: listAgents(url.searchParams.get("project_id") ?? undefined) });
|
|
81568
82034
|
}
|
|
81569
82035
|
if (path === "/api/agents" && method === "POST") {
|
|
81570
|
-
const
|
|
82036
|
+
const parsed = await safeJson(req);
|
|
82037
|
+
if ("error" in parsed)
|
|
82038
|
+
return parsed.error;
|
|
82039
|
+
const body = parsed.body;
|
|
81571
82040
|
if (!body.name)
|
|
81572
|
-
return badRequest("name required");
|
|
82041
|
+
return badRequest("name required", headers);
|
|
81573
82042
|
const agent = registerAgent2(body.name, { description: body.description, projectId: body.project_id, sessionId: body.session_id, workingDir: body.working_dir });
|
|
81574
82043
|
return ok({ agent }, 201);
|
|
81575
82044
|
}
|
|
@@ -81588,9 +82057,12 @@ var init_server = __esm(() => {
|
|
|
81588
82057
|
return ok({ projects: listProjects() });
|
|
81589
82058
|
}
|
|
81590
82059
|
if (path === "/api/projects" && method === "POST") {
|
|
81591
|
-
const
|
|
82060
|
+
const parsed = await safeJson(req);
|
|
82061
|
+
if ("error" in parsed)
|
|
82062
|
+
return parsed.error;
|
|
82063
|
+
const body = parsed.body;
|
|
81592
82064
|
if (!body.name || !body.path)
|
|
81593
|
-
return badRequest("name and path required");
|
|
82065
|
+
return badRequest("name and path required", headers);
|
|
81594
82066
|
const project = ensureProject(body.name, body.path, body.description);
|
|
81595
82067
|
return ok({ project }, 201);
|
|
81596
82068
|
}
|
|
@@ -81606,21 +82078,30 @@ var init_server = __esm(() => {
|
|
|
81606
82078
|
return ok(getGalleryStats(url.searchParams.get("project_id") ?? undefined));
|
|
81607
82079
|
}
|
|
81608
82080
|
if (path === "/api/gallery/diff" && method === "POST") {
|
|
81609
|
-
const
|
|
82081
|
+
const parsed = await safeJson(req);
|
|
82082
|
+
if ("error" in parsed)
|
|
82083
|
+
return parsed.error;
|
|
82084
|
+
const body = parsed.body;
|
|
81610
82085
|
const e1 = getEntry(body.id1);
|
|
81611
82086
|
const e2 = getEntry(body.id2);
|
|
81612
82087
|
if (!e1 || !e2)
|
|
81613
|
-
return notFound("Gallery entry not found");
|
|
82088
|
+
return notFound("Gallery entry not found", headers);
|
|
81614
82089
|
return ok(await diffImages(e1.path, e2.path));
|
|
81615
82090
|
}
|
|
81616
82091
|
if (path.match(/^\/api\/gallery\/([^/]+)\/tag$/) && method === "POST") {
|
|
82092
|
+
const parsed = await safeJson(req);
|
|
82093
|
+
if ("error" in parsed)
|
|
82094
|
+
return parsed.error;
|
|
82095
|
+
const body = parsed.body;
|
|
81617
82096
|
const id = path.split("/")[3];
|
|
81618
|
-
const body = await req.json();
|
|
81619
82097
|
return ok({ entry: tagEntry(id, body.tag) });
|
|
81620
82098
|
}
|
|
81621
82099
|
if (path.match(/^\/api\/gallery\/([^/]+)\/favorite$/) && method === "PUT") {
|
|
82100
|
+
const parsed = await safeJson(req);
|
|
82101
|
+
if ("error" in parsed)
|
|
82102
|
+
return parsed.error;
|
|
82103
|
+
const body = parsed.body;
|
|
81622
82104
|
const id = path.split("/")[3];
|
|
81623
|
-
const body = await req.json();
|
|
81624
82105
|
return ok({ entry: favoriteEntry(id, body.favorited) });
|
|
81625
82106
|
}
|
|
81626
82107
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
@@ -81628,14 +82109,14 @@ var init_server = __esm(() => {
|
|
|
81628
82109
|
const entry = getEntry(id);
|
|
81629
82110
|
if (!entry?.thumbnail_path || !existsSync19(entry.thumbnail_path))
|
|
81630
82111
|
return notFound("Thumbnail not found");
|
|
81631
|
-
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...
|
|
82112
|
+
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...headers } });
|
|
81632
82113
|
}
|
|
81633
82114
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
81634
82115
|
const id = path.split("/")[3];
|
|
81635
82116
|
const entry = getEntry(id);
|
|
81636
82117
|
if (!entry?.path || !existsSync19(entry.path))
|
|
81637
82118
|
return notFound("Image not found");
|
|
81638
|
-
return new Response(Bun.file(entry.path), { headers: { ...
|
|
82119
|
+
return new Response(Bun.file(entry.path), { headers: { ...headers } });
|
|
81639
82120
|
}
|
|
81640
82121
|
if (path.match(/^\/api\/gallery\/([^/]+)$/) && method === "DELETE") {
|
|
81641
82122
|
const id = path.split("/")[3];
|
|
@@ -81663,7 +82144,7 @@ var init_server = __esm(() => {
|
|
|
81663
82144
|
const file = getDownload(id);
|
|
81664
82145
|
if (!file || !existsSync19(file.path))
|
|
81665
82146
|
return notFound("Download not found");
|
|
81666
|
-
return new Response(Bun.file(file.path), { headers: { ...
|
|
82147
|
+
return new Response(Bun.file(file.path), { headers: { ...headers } });
|
|
81667
82148
|
}
|
|
81668
82149
|
if (path.match(/^\/api\/downloads\/([^/]+)$/) && method === "DELETE") {
|
|
81669
82150
|
const id = path.split("/")[3];
|
|
@@ -81671,15 +82152,21 @@ var init_server = __esm(() => {
|
|
|
81671
82152
|
}
|
|
81672
82153
|
const dashboardDist = join28(import.meta.dir, "../../dashboard/dist");
|
|
81673
82154
|
if (existsSync19(dashboardDist)) {
|
|
81674
|
-
const
|
|
82155
|
+
const cleanPath = path.replace(/^\//, "");
|
|
82156
|
+
if (cleanPath.includes("..") || cleanPath.startsWith("/"))
|
|
82157
|
+
return notFound("Not found", headers);
|
|
82158
|
+
const filePath = path === "/" ? join28(dashboardDist, "index.html") : join28(dashboardDist, cleanPath);
|
|
82159
|
+
const resolved = await Bun.file(filePath).arrayBuffer().then(() => join28(dashboardDist, cleanPath)) || "";
|
|
82160
|
+
if (!resolved.startsWith(dashboardDist))
|
|
82161
|
+
return notFound("Not found", headers);
|
|
81675
82162
|
if (existsSync19(filePath)) {
|
|
81676
|
-
return new Response(Bun.file(filePath), { headers
|
|
82163
|
+
return new Response(Bun.file(filePath), { headers });
|
|
81677
82164
|
}
|
|
81678
|
-
return new Response(Bun.file(join28(dashboardDist, "index.html")), { headers
|
|
82165
|
+
return new Response(Bun.file(join28(dashboardDist, "index.html")), { headers });
|
|
81679
82166
|
}
|
|
81680
82167
|
if (path === "/" || path === "") {
|
|
81681
82168
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|
|
81682
|
-
headers: { "Content-Type": "text/plain", ...
|
|
82169
|
+
headers: { "Content-Type": "text/plain", ...headers }
|
|
81683
82170
|
});
|
|
81684
82171
|
}
|
|
81685
82172
|
return notFound(`Route not found: ${method} ${path}`);
|
|
@@ -82142,6 +82629,7 @@ init_projects();
|
|
|
82142
82629
|
init_recorder();
|
|
82143
82630
|
init_recordings();
|
|
82144
82631
|
init_lightpanda();
|
|
82632
|
+
init_types2();
|
|
82145
82633
|
import chalk4 from "chalk";
|
|
82146
82634
|
function register13(program2) {
|
|
82147
82635
|
const recordCmd = program2.command("record").description("Manage action recordings");
|
|
@@ -82219,9 +82707,10 @@ function register13(program2) {
|
|
|
82219
82707
|
console.log(chalk4.blue(` Page: ${title} (${url})`));
|
|
82220
82708
|
}
|
|
82221
82709
|
});
|
|
82222
|
-
program2.command("login <url>").description("Login to a site: detect form, fill credentials from secrets, save auth state").option("--email <email>", "Email to login with").option("--save-as <name>", "Name to save storage state as").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
|
|
82223
|
-
const { session, page } = await createSession2({ engine: opts.engine, headless: !opts.headed });
|
|
82710
|
+
program2.command("login <url>").description("Login to a site: detect form, fill credentials from secrets, save auth state").option("--email <email>", "Email to login with").option("--password <password>", "Password to login with").option("--save-as <name>", "Name to save storage state as").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
|
|
82711
|
+
const { session, page } = await createSession2({ engine: opts.engine, useCase: "auth_flow" /* AUTH_FLOW */, headless: !opts.headed });
|
|
82224
82712
|
await navigate(page, url);
|
|
82713
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
82225
82714
|
const formInfo = await page.evaluate(() => {
|
|
82226
82715
|
const emailInput = document.querySelector('input[type="email"], input[name="email"], input[name="username"], input[autocomplete="email"], input[autocomplete="username"]');
|
|
82227
82716
|
const passwordInput = document.querySelector('input[type="password"]');
|
|
@@ -82243,15 +82732,15 @@ function register13(program2) {
|
|
|
82243
82732
|
console.log(chalk4.gray(` Submit button: ${formInfo.hasSubmitButton ? "\u2713" : "\u2717"}`));
|
|
82244
82733
|
}
|
|
82245
82734
|
let email = opts.email;
|
|
82246
|
-
let password;
|
|
82247
|
-
if (!email) {
|
|
82735
|
+
let password = opts.password;
|
|
82736
|
+
if (!email || !password) {
|
|
82248
82737
|
try {
|
|
82249
82738
|
const { getCredentials: getCredentials2 } = await Promise.resolve().then(() => (init_auth(), exports_auth));
|
|
82250
82739
|
const hostname2 = new URL(url).hostname;
|
|
82251
82740
|
const creds = await getCredentials2(hostname2);
|
|
82252
82741
|
if (creds) {
|
|
82253
|
-
email = creds.email ?? creds.username;
|
|
82254
|
-
password = creds.password;
|
|
82742
|
+
email = email ?? creds.email ?? creds.username;
|
|
82743
|
+
password = password ?? creds.password;
|
|
82255
82744
|
if (!opts.json)
|
|
82256
82745
|
console.log(chalk4.blue(` Credentials found for ${hostname2}`));
|
|
82257
82746
|
}
|