@hasna/browser 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1254 -349
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/index.js +497 -214
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/sanitize.d.ts +21 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/self-heal.d.ts +18 -0
- package/dist/lib/self-heal.d.ts.map +1 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/storage-state.d.ts +15 -0
- package/dist/lib/storage-state.d.ts.map +1 -0
- package/dist/lib/vision-fallback.d.ts +29 -0
- package/dist/lib/vision-fallback.d.ts.map +1 -0
- package/dist/mcp/index.js +1360 -470
- package/dist/server/index.js +374 -158
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -307,6 +307,23 @@ function runMigrations(db) {
|
|
|
307
307
|
);
|
|
308
308
|
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
309
309
|
`
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
version: 6,
|
|
313
|
+
sql: `
|
|
314
|
+
CREATE TABLE IF NOT EXISTS auth_flows (
|
|
315
|
+
id TEXT PRIMARY KEY,
|
|
316
|
+
name TEXT NOT NULL UNIQUE,
|
|
317
|
+
domain TEXT NOT NULL,
|
|
318
|
+
recording_id TEXT REFERENCES recordings(id),
|
|
319
|
+
storage_state_path TEXT,
|
|
320
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
321
|
+
last_used TEXT
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
|
|
325
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
|
|
326
|
+
`
|
|
310
327
|
}
|
|
311
328
|
];
|
|
312
329
|
for (const m of migrations) {
|
|
@@ -1418,6 +1435,188 @@ var init_dialogs = __esm(() => {
|
|
|
1418
1435
|
pendingDialogs = new Map;
|
|
1419
1436
|
});
|
|
1420
1437
|
|
|
1438
|
+
// src/engines/cdp.ts
|
|
1439
|
+
var exports_cdp = {};
|
|
1440
|
+
__export(exports_cdp, {
|
|
1441
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
1442
|
+
CDPClient: () => CDPClient
|
|
1443
|
+
});
|
|
1444
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
1445
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
1446
|
+
try {
|
|
1447
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
1448
|
+
} catch (err) {
|
|
1449
|
+
throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
class CDPClient {
|
|
1454
|
+
session;
|
|
1455
|
+
networkEnabled = false;
|
|
1456
|
+
performanceEnabled = false;
|
|
1457
|
+
constructor(session) {
|
|
1458
|
+
this.session = session;
|
|
1459
|
+
}
|
|
1460
|
+
static async fromPage(page) {
|
|
1461
|
+
try {
|
|
1462
|
+
const session = await page.context().newCDPSession(page);
|
|
1463
|
+
return new CDPClient(session);
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
async send(method, params) {
|
|
1469
|
+
try {
|
|
1470
|
+
return await this.session.send(method, params);
|
|
1471
|
+
} catch (err) {
|
|
1472
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
on(event, handler) {
|
|
1476
|
+
this.session.on(event, handler);
|
|
1477
|
+
}
|
|
1478
|
+
off(event, handler) {
|
|
1479
|
+
this.session.off(event, handler);
|
|
1480
|
+
}
|
|
1481
|
+
async enableNetwork() {
|
|
1482
|
+
if (!this.networkEnabled) {
|
|
1483
|
+
await this.send("Network.enable");
|
|
1484
|
+
this.networkEnabled = true;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
async enablePerformance() {
|
|
1488
|
+
if (!this.performanceEnabled) {
|
|
1489
|
+
await this.send("Performance.enable");
|
|
1490
|
+
this.performanceEnabled = true;
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
async getPerformanceMetrics() {
|
|
1494
|
+
await this.enablePerformance();
|
|
1495
|
+
const result = await this.send("Performance.getMetrics");
|
|
1496
|
+
const m = {};
|
|
1497
|
+
for (const metric of result.metrics) {
|
|
1498
|
+
m[metric.name] = metric.value;
|
|
1499
|
+
}
|
|
1500
|
+
return {
|
|
1501
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
1502
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
1503
|
+
dom_interactive: m["DOMInteractive"],
|
|
1504
|
+
dom_complete: m["DOMComplete"],
|
|
1505
|
+
load_event: m["LoadEventEnd"]
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
async startJSCoverage() {
|
|
1509
|
+
await this.send("Profiler.enable");
|
|
1510
|
+
await this.send("Debugger.enable");
|
|
1511
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
1512
|
+
callCount: false,
|
|
1513
|
+
detailed: true
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
async stopJSCoverage() {
|
|
1517
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
1518
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
1519
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
1520
|
+
url: r.url,
|
|
1521
|
+
text: "",
|
|
1522
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
1523
|
+
}));
|
|
1524
|
+
}
|
|
1525
|
+
async getCoverage() {
|
|
1526
|
+
await this.startJSCoverage();
|
|
1527
|
+
const js = await this.stopJSCoverage();
|
|
1528
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
1529
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
1530
|
+
}
|
|
1531
|
+
async captureHAREntries(page, handler) {
|
|
1532
|
+
await this.enableNetwork();
|
|
1533
|
+
const requestTimings = new Map;
|
|
1534
|
+
const onRequest = (params) => {
|
|
1535
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
1536
|
+
};
|
|
1537
|
+
const onResponse = (params) => {
|
|
1538
|
+
const start = requestTimings.get(params.requestId);
|
|
1539
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
1540
|
+
handler({
|
|
1541
|
+
method: "GET",
|
|
1542
|
+
url: params.response.url,
|
|
1543
|
+
status: params.response.status,
|
|
1544
|
+
duration
|
|
1545
|
+
});
|
|
1546
|
+
};
|
|
1547
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
1548
|
+
this.on("Network.responseReceived", onResponse);
|
|
1549
|
+
return () => {
|
|
1550
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
1551
|
+
this.off("Network.responseReceived", onResponse);
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
async detach() {
|
|
1555
|
+
try {
|
|
1556
|
+
await this.session.detach();
|
|
1557
|
+
} catch {}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
var init_cdp = __esm(() => {
|
|
1561
|
+
init_types();
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
// src/lib/storage-state.ts
|
|
1565
|
+
var exports_storage_state = {};
|
|
1566
|
+
__export(exports_storage_state, {
|
|
1567
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
1568
|
+
saveState: () => saveState,
|
|
1569
|
+
loadStatePath: () => loadStatePath,
|
|
1570
|
+
listStates: () => listStates,
|
|
1571
|
+
deleteState: () => deleteState
|
|
1572
|
+
});
|
|
1573
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
1574
|
+
import { join as join3 } from "path";
|
|
1575
|
+
import { homedir as homedir3 } from "os";
|
|
1576
|
+
function ensureDir() {
|
|
1577
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
1578
|
+
}
|
|
1579
|
+
function statePath(name) {
|
|
1580
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
1581
|
+
}
|
|
1582
|
+
async function saveState(context, name) {
|
|
1583
|
+
ensureDir();
|
|
1584
|
+
const path = statePath(name);
|
|
1585
|
+
const state = await context.storageState({ path });
|
|
1586
|
+
return path;
|
|
1587
|
+
}
|
|
1588
|
+
async function saveStateFromPage(page, name) {
|
|
1589
|
+
return saveState(page.context(), name);
|
|
1590
|
+
}
|
|
1591
|
+
function loadStatePath(name) {
|
|
1592
|
+
const path = statePath(name);
|
|
1593
|
+
return existsSync(path) ? path : null;
|
|
1594
|
+
}
|
|
1595
|
+
function listStates() {
|
|
1596
|
+
ensureDir();
|
|
1597
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
1598
|
+
const path = join3(STATES_DIR, f);
|
|
1599
|
+
const stat = Bun.file(path);
|
|
1600
|
+
return {
|
|
1601
|
+
name: f.replace(".json", ""),
|
|
1602
|
+
path,
|
|
1603
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
1604
|
+
};
|
|
1605
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
1606
|
+
}
|
|
1607
|
+
function deleteState(name) {
|
|
1608
|
+
const path = statePath(name);
|
|
1609
|
+
if (existsSync(path)) {
|
|
1610
|
+
unlinkSync(path);
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
var STATES_DIR;
|
|
1616
|
+
var init_storage_state = __esm(() => {
|
|
1617
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1421
1620
|
// src/lib/session.ts
|
|
1422
1621
|
var exports_session = {};
|
|
1423
1622
|
__export(exports_session, {
|
|
@@ -1447,6 +1646,37 @@ function createBunProxy(view) {
|
|
|
1447
1646
|
return view;
|
|
1448
1647
|
}
|
|
1449
1648
|
async function createSession2(opts = {}) {
|
|
1649
|
+
if (opts.cdpUrl) {
|
|
1650
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
1651
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
1652
|
+
const contexts = cdpBrowser.contexts();
|
|
1653
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
1654
|
+
const pages = context.pages();
|
|
1655
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
1656
|
+
const session2 = createSession({
|
|
1657
|
+
engine: "cdp",
|
|
1658
|
+
projectId: opts.projectId,
|
|
1659
|
+
agentId: opts.agentId,
|
|
1660
|
+
startUrl: page2.url(),
|
|
1661
|
+
name: opts.name ?? "attached"
|
|
1662
|
+
});
|
|
1663
|
+
const cleanups2 = [];
|
|
1664
|
+
if (opts.captureNetwork !== false) {
|
|
1665
|
+
try {
|
|
1666
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
1667
|
+
} catch {}
|
|
1668
|
+
}
|
|
1669
|
+
if (opts.captureConsole !== false) {
|
|
1670
|
+
try {
|
|
1671
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
1672
|
+
} catch {}
|
|
1673
|
+
}
|
|
1674
|
+
try {
|
|
1675
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
1676
|
+
} catch {}
|
|
1677
|
+
handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
|
|
1678
|
+
return { session: session2, page: page2 };
|
|
1679
|
+
}
|
|
1450
1680
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
1451
1681
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
1452
1682
|
let browser = null;
|
|
@@ -1472,7 +1702,22 @@ async function createSession2(opts = {}) {
|
|
|
1472
1702
|
page = await context.newPage();
|
|
1473
1703
|
} else {
|
|
1474
1704
|
browser = await pool.acquire(opts.headless ?? true);
|
|
1475
|
-
|
|
1705
|
+
if (opts.storageState) {
|
|
1706
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
1707
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
1708
|
+
if (statePath2) {
|
|
1709
|
+
const context = await browser.newContext({
|
|
1710
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
1711
|
+
userAgent: opts.userAgent,
|
|
1712
|
+
storageState: statePath2
|
|
1713
|
+
});
|
|
1714
|
+
page = await context.newPage();
|
|
1715
|
+
} else {
|
|
1716
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
1717
|
+
}
|
|
1718
|
+
} else {
|
|
1719
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
1720
|
+
}
|
|
1476
1721
|
}
|
|
1477
1722
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
1478
1723
|
try {
|
|
@@ -1977,6 +2222,66 @@ var init_snapshot = __esm(() => {
|
|
|
1977
2222
|
];
|
|
1978
2223
|
});
|
|
1979
2224
|
|
|
2225
|
+
// src/lib/self-heal.ts
|
|
2226
|
+
async function healSelector(page, selector, sessionId) {
|
|
2227
|
+
const attempts = [];
|
|
2228
|
+
attempts.push(`selector: ${selector}`);
|
|
2229
|
+
try {
|
|
2230
|
+
const loc = page.locator(selector).first();
|
|
2231
|
+
if (await loc.count() > 0) {
|
|
2232
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
2233
|
+
}
|
|
2234
|
+
} catch {}
|
|
2235
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
2236
|
+
attempts.push(`text: "${selector}"`);
|
|
2237
|
+
try {
|
|
2238
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
2239
|
+
if (await loc.count() > 0) {
|
|
2240
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
2241
|
+
}
|
|
2242
|
+
} catch {}
|
|
2243
|
+
}
|
|
2244
|
+
const roleMap = {
|
|
2245
|
+
button: ["button", "submit", "reset"],
|
|
2246
|
+
link: ["a"],
|
|
2247
|
+
input: ["input", "textarea"],
|
|
2248
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
2249
|
+
};
|
|
2250
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
2251
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
2252
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
2253
|
+
try {
|
|
2254
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
2255
|
+
if (await loc.count() > 0) {
|
|
2256
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
2257
|
+
}
|
|
2258
|
+
} catch {}
|
|
2259
|
+
}
|
|
2260
|
+
if (selector.startsWith("#")) {
|
|
2261
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
2262
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
2263
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
2264
|
+
try {
|
|
2265
|
+
const loc = page.locator(partialSel).first();
|
|
2266
|
+
if (await loc.count() > 0) {
|
|
2267
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
2268
|
+
}
|
|
2269
|
+
} catch {}
|
|
2270
|
+
}
|
|
2271
|
+
if (selector.startsWith(".")) {
|
|
2272
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
2273
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
2274
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
2275
|
+
try {
|
|
2276
|
+
const loc = page.locator(partialSel).first();
|
|
2277
|
+
if (await loc.count() > 0) {
|
|
2278
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
2279
|
+
}
|
|
2280
|
+
} catch {}
|
|
2281
|
+
}
|
|
2282
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1980
2285
|
// src/lib/actions.ts
|
|
1981
2286
|
var exports_actions = {};
|
|
1982
2287
|
__export(exports_actions, {
|
|
@@ -2018,11 +2323,22 @@ async function click(page, selector, opts) {
|
|
|
2018
2323
|
delay: opts?.delay,
|
|
2019
2324
|
timeout: opts?.timeout ?? 1e4
|
|
2020
2325
|
});
|
|
2021
|
-
|
|
2022
|
-
|
|
2326
|
+
return {};
|
|
2327
|
+
} catch (originalError) {
|
|
2328
|
+
if (opts?.selfHeal !== false) {
|
|
2329
|
+
const result = await healSelector(page, selector);
|
|
2330
|
+
if (result.found && result.locator) {
|
|
2331
|
+
await result.locator.click({
|
|
2332
|
+
button: opts?.button ?? "left",
|
|
2333
|
+
timeout: opts?.timeout ?? 1e4
|
|
2334
|
+
});
|
|
2335
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
2023
2339
|
throw new ElementNotFoundError(selector);
|
|
2024
2340
|
}
|
|
2025
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
2341
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
2026
2342
|
}
|
|
2027
2343
|
}
|
|
2028
2344
|
async function type(page, selector, text, opts) {
|
|
@@ -2031,17 +2347,35 @@ async function type(page, selector, text, opts) {
|
|
|
2031
2347
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
2032
2348
|
}
|
|
2033
2349
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
2034
|
-
|
|
2035
|
-
|
|
2350
|
+
return {};
|
|
2351
|
+
} catch (originalError) {
|
|
2352
|
+
if (opts?.selfHeal !== false) {
|
|
2353
|
+
const result = await healSelector(page, selector);
|
|
2354
|
+
if (result.found && result.locator) {
|
|
2355
|
+
if (opts?.clear)
|
|
2356
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
2357
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
2358
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
2036
2362
|
throw new ElementNotFoundError(selector);
|
|
2037
2363
|
}
|
|
2038
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
2364
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
2039
2365
|
}
|
|
2040
2366
|
}
|
|
2041
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
2367
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
2042
2368
|
try {
|
|
2043
2369
|
await page.fill(selector, value, { timeout });
|
|
2044
|
-
|
|
2370
|
+
return {};
|
|
2371
|
+
} catch (originalError) {
|
|
2372
|
+
if (selfHeal) {
|
|
2373
|
+
const result = await healSelector(page, selector);
|
|
2374
|
+
if (result.found && result.locator) {
|
|
2375
|
+
await result.locator.fill(value, { timeout });
|
|
2376
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2045
2379
|
throw new ElementNotFoundError(selector);
|
|
2046
2380
|
}
|
|
2047
2381
|
}
|
|
@@ -2166,12 +2500,39 @@ async function clickText(page, text, opts) {
|
|
|
2166
2500
|
}
|
|
2167
2501
|
}, { retries: opts?.retries ?? 1 });
|
|
2168
2502
|
}
|
|
2169
|
-
async function fillForm(page, fields, submitSelector) {
|
|
2503
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
2170
2504
|
let filled = 0;
|
|
2171
2505
|
const errors2 = [];
|
|
2506
|
+
const healedFields = [];
|
|
2172
2507
|
for (const [selector, value] of Object.entries(fields)) {
|
|
2173
2508
|
try {
|
|
2174
|
-
|
|
2509
|
+
let el = await page.$(selector);
|
|
2510
|
+
if (!el && selfHeal) {
|
|
2511
|
+
const result = await healSelector(page, selector);
|
|
2512
|
+
if (result.found && result.locator) {
|
|
2513
|
+
const handle = await result.locator.elementHandle();
|
|
2514
|
+
if (handle) {
|
|
2515
|
+
el = handle;
|
|
2516
|
+
healedFields.push(selector);
|
|
2517
|
+
const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
|
|
2518
|
+
const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
|
|
2519
|
+
if (tagName2 === "select") {
|
|
2520
|
+
await result.locator.selectOption(String(value));
|
|
2521
|
+
} else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
|
|
2522
|
+
if (Boolean(value))
|
|
2523
|
+
await result.locator.check();
|
|
2524
|
+
else
|
|
2525
|
+
await result.locator.uncheck();
|
|
2526
|
+
} else {
|
|
2527
|
+
await result.locator.fill(String(value));
|
|
2528
|
+
}
|
|
2529
|
+
filled++;
|
|
2530
|
+
continue;
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
errors2.push(`${selector}: element not found`);
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2175
2536
|
if (!el) {
|
|
2176
2537
|
errors2.push(`${selector}: element not found`);
|
|
2177
2538
|
continue;
|
|
@@ -2198,11 +2559,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
2198
2559
|
if (submitSelector) {
|
|
2199
2560
|
try {
|
|
2200
2561
|
await page.click(submitSelector);
|
|
2201
|
-
} catch (
|
|
2202
|
-
|
|
2562
|
+
} catch (submitErr) {
|
|
2563
|
+
if (selfHeal) {
|
|
2564
|
+
const result = await healSelector(page, submitSelector);
|
|
2565
|
+
if (result.found && result.locator) {
|
|
2566
|
+
await result.locator.click();
|
|
2567
|
+
healedFields.push(submitSelector);
|
|
2568
|
+
} else {
|
|
2569
|
+
errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
2570
|
+
}
|
|
2571
|
+
} else {
|
|
2572
|
+
errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
2573
|
+
}
|
|
2203
2574
|
}
|
|
2204
2575
|
}
|
|
2205
|
-
return { filled, errors: errors2, fields_attempted: Object.keys(fields).length };
|
|
2576
|
+
return { filled, errors: errors2, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
|
|
2206
2577
|
}
|
|
2207
2578
|
async function waitForText(page, text, opts) {
|
|
2208
2579
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -9057,6 +9428,237 @@ var init_gallery = __esm(() => {
|
|
|
9057
9428
|
init_schema();
|
|
9058
9429
|
});
|
|
9059
9430
|
|
|
9431
|
+
// src/db/recordings.ts
|
|
9432
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9433
|
+
function deserialize2(row) {
|
|
9434
|
+
return {
|
|
9435
|
+
...row,
|
|
9436
|
+
project_id: row.project_id ?? undefined,
|
|
9437
|
+
start_url: row.start_url ?? undefined,
|
|
9438
|
+
steps: JSON.parse(row.steps)
|
|
9439
|
+
};
|
|
9440
|
+
}
|
|
9441
|
+
function createRecording(data) {
|
|
9442
|
+
const db = getDatabase();
|
|
9443
|
+
const id = randomUUID5();
|
|
9444
|
+
db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
|
|
9445
|
+
return getRecording(id);
|
|
9446
|
+
}
|
|
9447
|
+
function getRecording(id) {
|
|
9448
|
+
const db = getDatabase();
|
|
9449
|
+
const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
|
|
9450
|
+
if (!row)
|
|
9451
|
+
throw new RecordingNotFoundError(id);
|
|
9452
|
+
return deserialize2(row);
|
|
9453
|
+
}
|
|
9454
|
+
function listRecordings(projectId) {
|
|
9455
|
+
const db = getDatabase();
|
|
9456
|
+
const rows = projectId ? db.query("SELECT * FROM recordings WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM recordings ORDER BY created_at DESC").all();
|
|
9457
|
+
return rows.map(deserialize2);
|
|
9458
|
+
}
|
|
9459
|
+
function updateRecording(id, data) {
|
|
9460
|
+
const db = getDatabase();
|
|
9461
|
+
const fields = [];
|
|
9462
|
+
const values = [];
|
|
9463
|
+
if (data.name !== undefined) {
|
|
9464
|
+
fields.push("name = ?");
|
|
9465
|
+
values.push(data.name);
|
|
9466
|
+
}
|
|
9467
|
+
if (data.steps !== undefined) {
|
|
9468
|
+
fields.push("steps = ?");
|
|
9469
|
+
values.push(JSON.stringify(data.steps));
|
|
9470
|
+
}
|
|
9471
|
+
if (data.start_url !== undefined) {
|
|
9472
|
+
fields.push("start_url = ?");
|
|
9473
|
+
values.push(data.start_url ?? null);
|
|
9474
|
+
}
|
|
9475
|
+
if (fields.length === 0)
|
|
9476
|
+
return getRecording(id);
|
|
9477
|
+
values.push(id);
|
|
9478
|
+
db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
9479
|
+
return getRecording(id);
|
|
9480
|
+
}
|
|
9481
|
+
var init_recordings = __esm(() => {
|
|
9482
|
+
init_schema();
|
|
9483
|
+
init_types();
|
|
9484
|
+
});
|
|
9485
|
+
|
|
9486
|
+
// src/lib/recorder.ts
|
|
9487
|
+
var exports_recorder = {};
|
|
9488
|
+
__export(exports_recorder, {
|
|
9489
|
+
stopRecording: () => stopRecording,
|
|
9490
|
+
startRecording: () => startRecording,
|
|
9491
|
+
replayRecording: () => replayRecording,
|
|
9492
|
+
recordStep: () => recordStep,
|
|
9493
|
+
listRecordings: () => listRecordings,
|
|
9494
|
+
getRecording: () => getRecording,
|
|
9495
|
+
exportRecording: () => exportRecording,
|
|
9496
|
+
attachPageListeners: () => attachPageListeners
|
|
9497
|
+
});
|
|
9498
|
+
function startRecording(sessionId, name, startUrl) {
|
|
9499
|
+
const steps = [];
|
|
9500
|
+
const recording = createRecording({ name, start_url: startUrl, steps });
|
|
9501
|
+
activeRecordings.set(recording.id, {
|
|
9502
|
+
id: recording.id,
|
|
9503
|
+
steps,
|
|
9504
|
+
cleanup: () => {}
|
|
9505
|
+
});
|
|
9506
|
+
return recording;
|
|
9507
|
+
}
|
|
9508
|
+
function attachPageListeners(page, recordingId) {
|
|
9509
|
+
const active = activeRecordings.get(recordingId);
|
|
9510
|
+
if (!active)
|
|
9511
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9512
|
+
const onFrameNav = () => {
|
|
9513
|
+
active.steps.push({
|
|
9514
|
+
type: "navigate",
|
|
9515
|
+
url: page.url(),
|
|
9516
|
+
timestamp: Date.now()
|
|
9517
|
+
});
|
|
9518
|
+
};
|
|
9519
|
+
page.on("framenavigated", onFrameNav);
|
|
9520
|
+
const cleanup = () => {
|
|
9521
|
+
page.off("framenavigated", onFrameNav);
|
|
9522
|
+
};
|
|
9523
|
+
active.cleanup = cleanup;
|
|
9524
|
+
}
|
|
9525
|
+
function recordStep(recordingId, step) {
|
|
9526
|
+
const active = activeRecordings.get(recordingId);
|
|
9527
|
+
if (!active)
|
|
9528
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9529
|
+
active.steps.push({ ...step, timestamp: Date.now() });
|
|
9530
|
+
}
|
|
9531
|
+
function stopRecording(recordingId) {
|
|
9532
|
+
const active = activeRecordings.get(recordingId);
|
|
9533
|
+
if (!active)
|
|
9534
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9535
|
+
active.cleanup();
|
|
9536
|
+
activeRecordings.delete(recordingId);
|
|
9537
|
+
return updateRecording(recordingId, { steps: active.steps });
|
|
9538
|
+
}
|
|
9539
|
+
async function replayRecording(recordingId, page) {
|
|
9540
|
+
const recording = getRecording(recordingId);
|
|
9541
|
+
const startTime = Date.now();
|
|
9542
|
+
let executed = 0;
|
|
9543
|
+
let failed = 0;
|
|
9544
|
+
const errors2 = [];
|
|
9545
|
+
for (const step of recording.steps) {
|
|
9546
|
+
try {
|
|
9547
|
+
switch (step.type) {
|
|
9548
|
+
case "navigate":
|
|
9549
|
+
if (step.url)
|
|
9550
|
+
await navigate(page, step.url);
|
|
9551
|
+
break;
|
|
9552
|
+
case "click":
|
|
9553
|
+
if (step.selector)
|
|
9554
|
+
await click(page, step.selector);
|
|
9555
|
+
break;
|
|
9556
|
+
case "type":
|
|
9557
|
+
if (step.selector && step.value)
|
|
9558
|
+
await type(page, step.selector, step.value);
|
|
9559
|
+
break;
|
|
9560
|
+
case "scroll":
|
|
9561
|
+
await scroll(page, "down");
|
|
9562
|
+
break;
|
|
9563
|
+
case "hover":
|
|
9564
|
+
if (step.selector) {
|
|
9565
|
+
const el = await page.$(step.selector);
|
|
9566
|
+
if (el)
|
|
9567
|
+
await el.hover();
|
|
9568
|
+
}
|
|
9569
|
+
break;
|
|
9570
|
+
case "evaluate":
|
|
9571
|
+
if (step.value)
|
|
9572
|
+
await page.evaluate(step.value);
|
|
9573
|
+
break;
|
|
9574
|
+
case "wait":
|
|
9575
|
+
if (step.selector) {
|
|
9576
|
+
await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
|
|
9577
|
+
}
|
|
9578
|
+
break;
|
|
9579
|
+
}
|
|
9580
|
+
executed++;
|
|
9581
|
+
} catch (err) {
|
|
9582
|
+
failed++;
|
|
9583
|
+
errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
9584
|
+
}
|
|
9585
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
9586
|
+
}
|
|
9587
|
+
return {
|
|
9588
|
+
recording_id: recordingId,
|
|
9589
|
+
success: failed === 0,
|
|
9590
|
+
steps_executed: executed,
|
|
9591
|
+
steps_failed: failed,
|
|
9592
|
+
errors: errors2,
|
|
9593
|
+
duration_ms: Date.now() - startTime
|
|
9594
|
+
};
|
|
9595
|
+
}
|
|
9596
|
+
function exportRecording(recordingId, format = "json") {
|
|
9597
|
+
const recording = getRecording(recordingId);
|
|
9598
|
+
if (format === "json") {
|
|
9599
|
+
return JSON.stringify(recording, null, 2);
|
|
9600
|
+
}
|
|
9601
|
+
if (format === "playwright") {
|
|
9602
|
+
const lines2 = [
|
|
9603
|
+
`import { test, expect } from '@playwright/test';`,
|
|
9604
|
+
``,
|
|
9605
|
+
`test('${recording.name}', async ({ page }) => {`
|
|
9606
|
+
];
|
|
9607
|
+
for (const step of recording.steps) {
|
|
9608
|
+
switch (step.type) {
|
|
9609
|
+
case "navigate":
|
|
9610
|
+
lines2.push(` await page.goto('${step.url}');`);
|
|
9611
|
+
break;
|
|
9612
|
+
case "click":
|
|
9613
|
+
lines2.push(` await page.click('${step.selector}');`);
|
|
9614
|
+
break;
|
|
9615
|
+
case "type":
|
|
9616
|
+
lines2.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
9617
|
+
break;
|
|
9618
|
+
case "scroll":
|
|
9619
|
+
lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
|
|
9620
|
+
break;
|
|
9621
|
+
case "evaluate":
|
|
9622
|
+
lines2.push(` await page.evaluate(${step.value});`);
|
|
9623
|
+
break;
|
|
9624
|
+
}
|
|
9625
|
+
}
|
|
9626
|
+
lines2.push(`});`);
|
|
9627
|
+
return lines2.join(`
|
|
9628
|
+
`);
|
|
9629
|
+
}
|
|
9630
|
+
const lines = [
|
|
9631
|
+
`const puppeteer = require('puppeteer');`,
|
|
9632
|
+
``,
|
|
9633
|
+
`(async () => {`,
|
|
9634
|
+
` const browser = await puppeteer.launch();`,
|
|
9635
|
+
` const page = await browser.newPage();`
|
|
9636
|
+
];
|
|
9637
|
+
for (const step of recording.steps) {
|
|
9638
|
+
switch (step.type) {
|
|
9639
|
+
case "navigate":
|
|
9640
|
+
lines.push(` await page.goto('${step.url}');`);
|
|
9641
|
+
break;
|
|
9642
|
+
case "click":
|
|
9643
|
+
lines.push(` await page.click('${step.selector}');`);
|
|
9644
|
+
break;
|
|
9645
|
+
case "type":
|
|
9646
|
+
lines.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
9647
|
+
break;
|
|
9648
|
+
}
|
|
9649
|
+
}
|
|
9650
|
+
lines.push(` await browser.close();`, `})();`);
|
|
9651
|
+
return lines.join(`
|
|
9652
|
+
`);
|
|
9653
|
+
}
|
|
9654
|
+
var activeRecordings;
|
|
9655
|
+
var init_recorder = __esm(() => {
|
|
9656
|
+
init_recordings();
|
|
9657
|
+
init_actions();
|
|
9658
|
+
init_types();
|
|
9659
|
+
activeRecordings = new Map;
|
|
9660
|
+
});
|
|
9661
|
+
|
|
9060
9662
|
// src/lib/profiles.ts
|
|
9061
9663
|
var exports_profiles = {};
|
|
9062
9664
|
__export(exports_profiles, {
|
|
@@ -9066,23 +9668,23 @@ __export(exports_profiles, {
|
|
|
9066
9668
|
deleteProfile: () => deleteProfile,
|
|
9067
9669
|
applyProfile: () => applyProfile
|
|
9068
9670
|
});
|
|
9069
|
-
import { mkdirSync as
|
|
9070
|
-
import { join as
|
|
9071
|
-
import { homedir as
|
|
9671
|
+
import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
9672
|
+
import { join as join8 } from "path";
|
|
9673
|
+
import { homedir as homedir8 } from "os";
|
|
9072
9674
|
function getProfilesDir() {
|
|
9073
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
9074
|
-
const dir =
|
|
9075
|
-
|
|
9675
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
|
|
9676
|
+
const dir = join8(dataDir, "profiles");
|
|
9677
|
+
mkdirSync8(dir, { recursive: true });
|
|
9076
9678
|
return dir;
|
|
9077
9679
|
}
|
|
9078
9680
|
function getProfileDir2(name) {
|
|
9079
|
-
return
|
|
9681
|
+
return join8(getProfilesDir(), name);
|
|
9080
9682
|
}
|
|
9081
9683
|
async function saveProfile(page, name) {
|
|
9082
9684
|
const dir = getProfileDir2(name);
|
|
9083
|
-
|
|
9685
|
+
mkdirSync8(dir, { recursive: true });
|
|
9084
9686
|
const cookies = await page.context().cookies();
|
|
9085
|
-
writeFileSync2(
|
|
9687
|
+
writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
9086
9688
|
let localStorage2 = {};
|
|
9087
9689
|
try {
|
|
9088
9690
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -9094,11 +9696,11 @@ async function saveProfile(page, name) {
|
|
|
9094
9696
|
return result;
|
|
9095
9697
|
});
|
|
9096
9698
|
} catch {}
|
|
9097
|
-
writeFileSync2(
|
|
9699
|
+
writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
9098
9700
|
const savedAt = new Date().toISOString();
|
|
9099
9701
|
const url = page.url();
|
|
9100
9702
|
const meta = { saved_at: savedAt, url };
|
|
9101
|
-
writeFileSync2(
|
|
9703
|
+
writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
9102
9704
|
return {
|
|
9103
9705
|
name,
|
|
9104
9706
|
saved_at: savedAt,
|
|
@@ -9109,17 +9711,17 @@ async function saveProfile(page, name) {
|
|
|
9109
9711
|
}
|
|
9110
9712
|
function loadProfile(name) {
|
|
9111
9713
|
const dir = getProfileDir2(name);
|
|
9112
|
-
if (!
|
|
9714
|
+
if (!existsSync4(dir)) {
|
|
9113
9715
|
throw new Error(`Profile not found: ${name}`);
|
|
9114
9716
|
}
|
|
9115
|
-
const cookiesPath =
|
|
9116
|
-
const storagePath =
|
|
9117
|
-
const metaPath2 =
|
|
9118
|
-
const cookies =
|
|
9119
|
-
const localStorage2 =
|
|
9717
|
+
const cookiesPath = join8(dir, "cookies.json");
|
|
9718
|
+
const storagePath = join8(dir, "storage.json");
|
|
9719
|
+
const metaPath2 = join8(dir, "meta.json");
|
|
9720
|
+
const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
9721
|
+
const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
9120
9722
|
let savedAt = new Date().toISOString();
|
|
9121
9723
|
let url;
|
|
9122
|
-
if (
|
|
9724
|
+
if (existsSync4(metaPath2)) {
|
|
9123
9725
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
9124
9726
|
savedAt = meta.saved_at ?? savedAt;
|
|
9125
9727
|
url = meta.url;
|
|
@@ -9147,33 +9749,33 @@ async function applyProfile(page, profileData) {
|
|
|
9147
9749
|
}
|
|
9148
9750
|
function listProfiles() {
|
|
9149
9751
|
const dir = getProfilesDir();
|
|
9150
|
-
if (!
|
|
9752
|
+
if (!existsSync4(dir))
|
|
9151
9753
|
return [];
|
|
9152
|
-
const entries =
|
|
9754
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
9153
9755
|
const profiles = [];
|
|
9154
9756
|
for (const entry of entries) {
|
|
9155
9757
|
if (!entry.isDirectory())
|
|
9156
9758
|
continue;
|
|
9157
9759
|
const name = entry.name;
|
|
9158
|
-
const profileDir =
|
|
9760
|
+
const profileDir = join8(dir, name);
|
|
9159
9761
|
let savedAt = "";
|
|
9160
9762
|
let url;
|
|
9161
9763
|
let cookieCount = 0;
|
|
9162
9764
|
let storageKeyCount = 0;
|
|
9163
9765
|
try {
|
|
9164
|
-
const metaPath2 =
|
|
9165
|
-
if (
|
|
9766
|
+
const metaPath2 = join8(profileDir, "meta.json");
|
|
9767
|
+
if (existsSync4(metaPath2)) {
|
|
9166
9768
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
9167
9769
|
savedAt = meta.saved_at ?? "";
|
|
9168
9770
|
url = meta.url;
|
|
9169
9771
|
}
|
|
9170
|
-
const cookiesPath =
|
|
9171
|
-
if (
|
|
9772
|
+
const cookiesPath = join8(profileDir, "cookies.json");
|
|
9773
|
+
if (existsSync4(cookiesPath)) {
|
|
9172
9774
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
9173
9775
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
9174
9776
|
}
|
|
9175
|
-
const storagePath =
|
|
9176
|
-
if (
|
|
9777
|
+
const storagePath = join8(profileDir, "storage.json");
|
|
9778
|
+
if (existsSync4(storagePath)) {
|
|
9177
9779
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
9178
9780
|
storageKeyCount = Object.keys(storage).length;
|
|
9179
9781
|
}
|
|
@@ -9190,7 +9792,7 @@ function listProfiles() {
|
|
|
9190
9792
|
}
|
|
9191
9793
|
function deleteProfile(name) {
|
|
9192
9794
|
const dir = getProfileDir2(name);
|
|
9193
|
-
if (!
|
|
9795
|
+
if (!existsSync4(dir))
|
|
9194
9796
|
return false;
|
|
9195
9797
|
try {
|
|
9196
9798
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -9201,6 +9803,97 @@ function deleteProfile(name) {
|
|
|
9201
9803
|
}
|
|
9202
9804
|
var init_profiles = () => {};
|
|
9203
9805
|
|
|
9806
|
+
// src/lib/sanitize.ts
|
|
9807
|
+
var exports_sanitize = {};
|
|
9808
|
+
__export(exports_sanitize, {
|
|
9809
|
+
sanitizeText: () => sanitizeText,
|
|
9810
|
+
sanitizeHTML: () => sanitizeHTML
|
|
9811
|
+
});
|
|
9812
|
+
function sanitizeText(text) {
|
|
9813
|
+
let stripped = 0;
|
|
9814
|
+
const warnings = [];
|
|
9815
|
+
let clean = text;
|
|
9816
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
9817
|
+
pattern.lastIndex = 0;
|
|
9818
|
+
const matches = clean.match(pattern);
|
|
9819
|
+
if (matches) {
|
|
9820
|
+
stripped += matches.length;
|
|
9821
|
+
warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
|
|
9822
|
+
pattern.lastIndex = 0;
|
|
9823
|
+
clean = clean.replace(pattern, "[STRIPPED]");
|
|
9824
|
+
}
|
|
9825
|
+
}
|
|
9826
|
+
return { text: clean, stripped, warnings };
|
|
9827
|
+
}
|
|
9828
|
+
function sanitizeHTML(html) {
|
|
9829
|
+
let stripped = 0;
|
|
9830
|
+
const warnings = [];
|
|
9831
|
+
let clean = html;
|
|
9832
|
+
const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
|
|
9833
|
+
if (commentMatches) {
|
|
9834
|
+
for (const comment of commentMatches) {
|
|
9835
|
+
if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
|
|
9836
|
+
stripped++;
|
|
9837
|
+
warnings.push(`Stripped HTML comment (${comment.length} chars)`);
|
|
9838
|
+
}
|
|
9839
|
+
}
|
|
9840
|
+
clean = clean.replace(/<!--[\s\S]*?-->/g, "");
|
|
9841
|
+
}
|
|
9842
|
+
const hiddenPatterns = [
|
|
9843
|
+
/style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9844
|
+
/style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9845
|
+
/style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9846
|
+
/style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9847
|
+
/style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
|
|
9848
|
+
];
|
|
9849
|
+
for (const pattern of hiddenPatterns) {
|
|
9850
|
+
pattern.lastIndex = 0;
|
|
9851
|
+
const matches = clean.match(pattern);
|
|
9852
|
+
if (matches) {
|
|
9853
|
+
stripped += matches.length;
|
|
9854
|
+
warnings.push(`Stripped ${matches.length} hidden elements`);
|
|
9855
|
+
pattern.lastIndex = 0;
|
|
9856
|
+
clean = clean.replace(pattern, "");
|
|
9857
|
+
}
|
|
9858
|
+
}
|
|
9859
|
+
const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
|
|
9860
|
+
const ariaHidden = clean.match(ariaHiddenPattern);
|
|
9861
|
+
if (ariaHidden) {
|
|
9862
|
+
stripped += ariaHidden.length;
|
|
9863
|
+
warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
|
|
9864
|
+
ariaHiddenPattern.lastIndex = 0;
|
|
9865
|
+
clean = clean.replace(ariaHiddenPattern, "");
|
|
9866
|
+
}
|
|
9867
|
+
const textResult = sanitizeText(clean);
|
|
9868
|
+
return {
|
|
9869
|
+
text: textResult.text,
|
|
9870
|
+
stripped: stripped + textResult.stripped,
|
|
9871
|
+
warnings: [...warnings, ...textResult.warnings]
|
|
9872
|
+
};
|
|
9873
|
+
}
|
|
9874
|
+
var INJECTION_PATTERNS;
|
|
9875
|
+
var init_sanitize = __esm(() => {
|
|
9876
|
+
INJECTION_PATTERNS = [
|
|
9877
|
+
/ignore\s+(all\s+)?previous\s+instructions/gi,
|
|
9878
|
+
/ignore\s+(all\s+)?prior\s+instructions/gi,
|
|
9879
|
+
/disregard\s+(all\s+)?previous/gi,
|
|
9880
|
+
/forget\s+(all\s+)?previous/gi,
|
|
9881
|
+
/you\s+are\s+now\s+/gi,
|
|
9882
|
+
/new\s+instructions?\s*:/gi,
|
|
9883
|
+
/system\s+prompt\s*:/gi,
|
|
9884
|
+
/\[INST\]/gi,
|
|
9885
|
+
/\[\/INST\]/gi,
|
|
9886
|
+
/<\|im_start\|>/gi,
|
|
9887
|
+
/<\|im_end\|>/gi,
|
|
9888
|
+
/<<SYS>>/gi,
|
|
9889
|
+
/<<\/SYS>>/gi,
|
|
9890
|
+
/IMPORTANT:\s*ignore/gi,
|
|
9891
|
+
/CRITICAL:\s*override/gi,
|
|
9892
|
+
/assistant:\s/gi,
|
|
9893
|
+
/human:\s/gi
|
|
9894
|
+
];
|
|
9895
|
+
});
|
|
9896
|
+
|
|
9204
9897
|
// src/lib/annotate.ts
|
|
9205
9898
|
var exports_annotate = {};
|
|
9206
9899
|
__export(exports_annotate, {
|
|
@@ -9261,26 +9954,213 @@ var init_annotate = __esm(() => {
|
|
|
9261
9954
|
import_sharp3 = __toESM(require_lib(), 1);
|
|
9262
9955
|
});
|
|
9263
9956
|
|
|
9957
|
+
// src/lib/auth-flow.ts
|
|
9958
|
+
var exports_auth_flow = {};
|
|
9959
|
+
__export(exports_auth_flow, {
|
|
9960
|
+
tryReplayAuth: () => tryReplayAuth,
|
|
9961
|
+
touchAuthFlow: () => touchAuthFlow,
|
|
9962
|
+
saveAuthFlow: () => saveAuthFlow,
|
|
9963
|
+
listAuthFlows: () => listAuthFlows,
|
|
9964
|
+
isAuthRedirect: () => isAuthRedirect,
|
|
9965
|
+
isAuthPage: () => isAuthPage,
|
|
9966
|
+
getAuthFlowByName: () => getAuthFlowByName,
|
|
9967
|
+
getAuthFlowByDomain: () => getAuthFlowByDomain,
|
|
9968
|
+
getAuthFlow: () => getAuthFlow,
|
|
9969
|
+
deleteAuthFlow: () => deleteAuthFlow
|
|
9970
|
+
});
|
|
9971
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
9972
|
+
function saveAuthFlow(data) {
|
|
9973
|
+
const db = getDatabase();
|
|
9974
|
+
const id = randomUUID10();
|
|
9975
|
+
db.prepare("INSERT OR REPLACE INTO auth_flows (id, name, domain, recording_id, storage_state_path) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.domain, data.recordingId ?? null, data.storageStatePath ?? null);
|
|
9976
|
+
return getAuthFlow(id);
|
|
9977
|
+
}
|
|
9978
|
+
function getAuthFlow(id) {
|
|
9979
|
+
const db = getDatabase();
|
|
9980
|
+
return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
|
|
9981
|
+
}
|
|
9982
|
+
function getAuthFlowByName(name) {
|
|
9983
|
+
const db = getDatabase();
|
|
9984
|
+
return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
|
|
9985
|
+
}
|
|
9986
|
+
function getAuthFlowByDomain(domain) {
|
|
9987
|
+
const db = getDatabase();
|
|
9988
|
+
return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
|
|
9989
|
+
}
|
|
9990
|
+
function listAuthFlows() {
|
|
9991
|
+
const db = getDatabase();
|
|
9992
|
+
return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
|
|
9993
|
+
}
|
|
9994
|
+
function deleteAuthFlow(name) {
|
|
9995
|
+
const db = getDatabase();
|
|
9996
|
+
const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
|
|
9997
|
+
return result.changes > 0;
|
|
9998
|
+
}
|
|
9999
|
+
function touchAuthFlow(id) {
|
|
10000
|
+
const db = getDatabase();
|
|
10001
|
+
db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
|
|
10002
|
+
}
|
|
10003
|
+
function isAuthPage(url) {
|
|
10004
|
+
return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
10005
|
+
}
|
|
10006
|
+
function isAuthRedirect(fromUrl, toUrl) {
|
|
10007
|
+
if (fromUrl === toUrl)
|
|
10008
|
+
return false;
|
|
10009
|
+
return isAuthPage(toUrl) && !isAuthPage(fromUrl);
|
|
10010
|
+
}
|
|
10011
|
+
async function tryReplayAuth(page, domain) {
|
|
10012
|
+
const flow = getAuthFlowByDomain(domain);
|
|
10013
|
+
if (!flow)
|
|
10014
|
+
return { replayed: false };
|
|
10015
|
+
if (flow.storage_state_path) {
|
|
10016
|
+
try {
|
|
10017
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
|
|
10018
|
+
if (existsSync5(flow.storage_state_path)) {
|
|
10019
|
+
const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
|
|
10020
|
+
if (state.cookies?.length) {
|
|
10021
|
+
await page.context().addCookies(state.cookies);
|
|
10022
|
+
await page.reload();
|
|
10023
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
10024
|
+
if (!isAuthPage(page.url())) {
|
|
10025
|
+
touchAuthFlow(flow.id);
|
|
10026
|
+
return { replayed: true, flow, method: "storage_state" };
|
|
10027
|
+
}
|
|
10028
|
+
}
|
|
10029
|
+
}
|
|
10030
|
+
} catch {}
|
|
10031
|
+
}
|
|
10032
|
+
if (flow.recording_id) {
|
|
10033
|
+
try {
|
|
10034
|
+
const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
10035
|
+
const result = await replayRecording2(flow.recording_id, page);
|
|
10036
|
+
if (result.success) {
|
|
10037
|
+
try {
|
|
10038
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
10039
|
+
const path = await saveStateFromPage2(page, flow.name);
|
|
10040
|
+
const db = getDatabase();
|
|
10041
|
+
db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
|
|
10042
|
+
} catch {}
|
|
10043
|
+
touchAuthFlow(flow.id);
|
|
10044
|
+
return { replayed: true, flow, method: "recording_replay" };
|
|
10045
|
+
}
|
|
10046
|
+
} catch {}
|
|
10047
|
+
}
|
|
10048
|
+
return { replayed: false, flow };
|
|
10049
|
+
}
|
|
10050
|
+
var AUTH_URL_PATTERNS;
|
|
10051
|
+
var init_auth_flow = __esm(() => {
|
|
10052
|
+
init_schema();
|
|
10053
|
+
AUTH_URL_PATTERNS = [
|
|
10054
|
+
/\/login/i,
|
|
10055
|
+
/\/signin/i,
|
|
10056
|
+
/\/sign-in/i,
|
|
10057
|
+
/\/auth/i,
|
|
10058
|
+
/\/sso/i,
|
|
10059
|
+
/\/oauth/i,
|
|
10060
|
+
/\/cas\/login/i,
|
|
10061
|
+
/accounts\.google\.com/i,
|
|
10062
|
+
/login\.microsoftonline\.com/i,
|
|
10063
|
+
/github\.com\/login/i,
|
|
10064
|
+
/auth0\.com/i
|
|
10065
|
+
];
|
|
10066
|
+
});
|
|
10067
|
+
|
|
10068
|
+
// src/lib/vision-fallback.ts
|
|
10069
|
+
var exports_vision_fallback = {};
|
|
10070
|
+
__export(exports_vision_fallback, {
|
|
10071
|
+
findElementByVision: () => findElementByVision,
|
|
10072
|
+
clickByVision: () => clickByVision
|
|
10073
|
+
});
|
|
10074
|
+
async function findElementByVision(page, description, opts) {
|
|
10075
|
+
const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
|
|
10076
|
+
const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
10077
|
+
const base64 = screenshot.toString("base64");
|
|
10078
|
+
const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
|
|
10079
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
10080
|
+
if (!apiKey) {
|
|
10081
|
+
return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
|
|
10082
|
+
}
|
|
10083
|
+
try {
|
|
10084
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
10085
|
+
method: "POST",
|
|
10086
|
+
headers: {
|
|
10087
|
+
"content-type": "application/json",
|
|
10088
|
+
"x-api-key": apiKey,
|
|
10089
|
+
"anthropic-version": "2023-06-01"
|
|
10090
|
+
},
|
|
10091
|
+
body: JSON.stringify({
|
|
10092
|
+
model,
|
|
10093
|
+
max_tokens: 256,
|
|
10094
|
+
messages: [{
|
|
10095
|
+
role: "user",
|
|
10096
|
+
content: [
|
|
10097
|
+
{
|
|
10098
|
+
type: "image",
|
|
10099
|
+
source: { type: "base64", media_type: "image/jpeg", data: base64 }
|
|
10100
|
+
},
|
|
10101
|
+
{
|
|
10102
|
+
type: "text",
|
|
10103
|
+
text: `Find the element matching this description: "${description}"
|
|
10104
|
+
|
|
10105
|
+
The screenshot is ${viewport.width}x${viewport.height} pixels.
|
|
10106
|
+
|
|
10107
|
+
Reply with ONLY a JSON object (no markdown, no explanation):
|
|
10108
|
+
{"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
|
|
10109
|
+
|
|
10110
|
+
If you cannot find the element:
|
|
10111
|
+
{"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
|
|
10112
|
+
}
|
|
10113
|
+
]
|
|
10114
|
+
}]
|
|
10115
|
+
})
|
|
10116
|
+
});
|
|
10117
|
+
const data = await response.json();
|
|
10118
|
+
const text = data.content?.[0]?.text ?? "";
|
|
10119
|
+
const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
|
|
10120
|
+
const result = JSON.parse(jsonStr);
|
|
10121
|
+
result.model = model;
|
|
10122
|
+
return result;
|
|
10123
|
+
} catch (err) {
|
|
10124
|
+
return {
|
|
10125
|
+
found: false,
|
|
10126
|
+
x: 0,
|
|
10127
|
+
y: 0,
|
|
10128
|
+
confidence: "none",
|
|
10129
|
+
description,
|
|
10130
|
+
model,
|
|
10131
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10132
|
+
};
|
|
10133
|
+
}
|
|
10134
|
+
}
|
|
10135
|
+
async function clickByVision(page, description, opts) {
|
|
10136
|
+
const result = await findElementByVision(page, description, opts);
|
|
10137
|
+
if (result.found && result.x > 0 && result.y > 0) {
|
|
10138
|
+
await page.mouse.click(result.x, result.y);
|
|
10139
|
+
}
|
|
10140
|
+
return result;
|
|
10141
|
+
}
|
|
10142
|
+
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
10143
|
+
|
|
9264
10144
|
// src/lib/auth.ts
|
|
9265
10145
|
var exports_auth = {};
|
|
9266
10146
|
__export(exports_auth, {
|
|
9267
10147
|
loginWithCredentials: () => loginWithCredentials,
|
|
9268
10148
|
getCredentials: () => getCredentials
|
|
9269
10149
|
});
|
|
9270
|
-
import { existsSync as
|
|
9271
|
-
import { join as
|
|
9272
|
-
import { homedir as
|
|
10150
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
10151
|
+
import { join as join9 } from "path";
|
|
10152
|
+
import { homedir as homedir9 } from "os";
|
|
9273
10153
|
async function getCredentials(service) {
|
|
9274
10154
|
try {
|
|
9275
|
-
const { getSecret } = await import(`${
|
|
10155
|
+
const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
|
|
9276
10156
|
const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
|
|
9277
10157
|
const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
|
|
9278
10158
|
if (email?.value && password?.value) {
|
|
9279
10159
|
return { email: email.value, password: password.value };
|
|
9280
10160
|
}
|
|
9281
10161
|
} catch {}
|
|
9282
|
-
const secretsPath =
|
|
9283
|
-
if (
|
|
10162
|
+
const secretsPath = join9(homedir9(), ".secrets");
|
|
10163
|
+
if (existsSync5(secretsPath)) {
|
|
9284
10164
|
const content = readFileSync3(secretsPath, "utf8");
|
|
9285
10165
|
const lines = content.split(`
|
|
9286
10166
|
`);
|
|
@@ -9457,14 +10337,14 @@ __export(exports_dist, {
|
|
|
9457
10337
|
InvalidScopeError: () => InvalidScopeError,
|
|
9458
10338
|
EntityNotFoundError: () => EntityNotFoundError,
|
|
9459
10339
|
DuplicateMemoryError: () => DuplicateMemoryError,
|
|
9460
|
-
DEFAULT_MODEL: () =>
|
|
10340
|
+
DEFAULT_MODEL: () => DEFAULT_MODEL2,
|
|
9461
10341
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG
|
|
9462
10342
|
});
|
|
9463
10343
|
import { Database as Database2 } from "bun:sqlite";
|
|
9464
|
-
import { existsSync as
|
|
9465
|
-
import { dirname, join as
|
|
9466
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as
|
|
9467
|
-
import { homedir as
|
|
10344
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
10345
|
+
import { dirname, join as join10, resolve } from "path";
|
|
10346
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
10347
|
+
import { homedir as homedir10 } from "os";
|
|
9468
10348
|
import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
|
|
9469
10349
|
import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
|
|
9470
10350
|
import { homedir as homedir22 } from "os";
|
|
@@ -9478,8 +10358,8 @@ function isInMemoryDb(path) {
|
|
|
9478
10358
|
function findNearestMementosDb(startDir) {
|
|
9479
10359
|
let dir = resolve(startDir);
|
|
9480
10360
|
while (true) {
|
|
9481
|
-
const candidate =
|
|
9482
|
-
if (
|
|
10361
|
+
const candidate = join10(dir, ".mementos", "mementos.db");
|
|
10362
|
+
if (existsSync6(candidate))
|
|
9483
10363
|
return candidate;
|
|
9484
10364
|
const parent = dirname(dir);
|
|
9485
10365
|
if (parent === dir)
|
|
@@ -9491,7 +10371,7 @@ function findNearestMementosDb(startDir) {
|
|
|
9491
10371
|
function findGitRoot(startDir) {
|
|
9492
10372
|
let dir = resolve(startDir);
|
|
9493
10373
|
while (true) {
|
|
9494
|
-
if (
|
|
10374
|
+
if (existsSync6(join10(dir, ".git")))
|
|
9495
10375
|
return dir;
|
|
9496
10376
|
const parent = dirname(dir);
|
|
9497
10377
|
if (parent === dir)
|
|
@@ -9511,25 +10391,25 @@ function getDbPath() {
|
|
|
9511
10391
|
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
9512
10392
|
const gitRoot = findGitRoot(cwd);
|
|
9513
10393
|
if (gitRoot) {
|
|
9514
|
-
return
|
|
10394
|
+
return join10(gitRoot, ".mementos", "mementos.db");
|
|
9515
10395
|
}
|
|
9516
10396
|
}
|
|
9517
10397
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
9518
|
-
return
|
|
10398
|
+
return join10(home, ".mementos", "mementos.db");
|
|
9519
10399
|
}
|
|
9520
|
-
function
|
|
10400
|
+
function ensureDir2(filePath) {
|
|
9521
10401
|
if (isInMemoryDb(filePath))
|
|
9522
10402
|
return;
|
|
9523
10403
|
const dir = dirname(resolve(filePath));
|
|
9524
|
-
if (!
|
|
9525
|
-
|
|
10404
|
+
if (!existsSync6(dir)) {
|
|
10405
|
+
mkdirSync9(dir, { recursive: true });
|
|
9526
10406
|
}
|
|
9527
10407
|
}
|
|
9528
10408
|
function getDatabase2(dbPath) {
|
|
9529
10409
|
if (_db2)
|
|
9530
10410
|
return _db2;
|
|
9531
10411
|
const path = dbPath || getDbPath();
|
|
9532
|
-
|
|
10412
|
+
ensureDir2(path);
|
|
9533
10413
|
_db2 = new Database2(path, { create: true });
|
|
9534
10414
|
_db2.run("PRAGMA journal_mode = WAL");
|
|
9535
10415
|
_db2.run("PRAGMA busy_timeout = 5000");
|
|
@@ -11372,7 +12252,7 @@ function isValidCategory(value) {
|
|
|
11372
12252
|
return VALID_CATEGORIES.includes(value);
|
|
11373
12253
|
}
|
|
11374
12254
|
function loadConfig() {
|
|
11375
|
-
const configPath = join22(
|
|
12255
|
+
const configPath = join22(homedir10(), ".mementos", "config.json");
|
|
11376
12256
|
let fileConfig = {};
|
|
11377
12257
|
if (existsSync22(configPath)) {
|
|
11378
12258
|
try {
|
|
@@ -11399,10 +12279,10 @@ function loadConfig() {
|
|
|
11399
12279
|
return merged;
|
|
11400
12280
|
}
|
|
11401
12281
|
function profilesDir() {
|
|
11402
|
-
return join22(
|
|
12282
|
+
return join22(homedir10(), ".mementos", "profiles");
|
|
11403
12283
|
}
|
|
11404
12284
|
function globalConfigPath() {
|
|
11405
|
-
return join22(
|
|
12285
|
+
return join22(homedir10(), ".mementos", "config.json");
|
|
11406
12286
|
}
|
|
11407
12287
|
function readGlobalConfig() {
|
|
11408
12288
|
const p = globalConfigPath();
|
|
@@ -11416,7 +12296,7 @@ function readGlobalConfig() {
|
|
|
11416
12296
|
}
|
|
11417
12297
|
function writeGlobalConfig(data) {
|
|
11418
12298
|
const p = globalConfigPath();
|
|
11419
|
-
|
|
12299
|
+
ensureDir22(dirname2(p));
|
|
11420
12300
|
writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
|
|
11421
12301
|
}
|
|
11422
12302
|
function getActiveProfile() {
|
|
@@ -11439,18 +12319,18 @@ function listProfiles2() {
|
|
|
11439
12319
|
const dir = profilesDir();
|
|
11440
12320
|
if (!existsSync22(dir))
|
|
11441
12321
|
return [];
|
|
11442
|
-
return
|
|
12322
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
|
|
11443
12323
|
}
|
|
11444
12324
|
function deleteProfile2(name) {
|
|
11445
12325
|
const dbPath = join22(profilesDir(), `${name}.db`);
|
|
11446
12326
|
if (!existsSync22(dbPath))
|
|
11447
12327
|
return false;
|
|
11448
|
-
|
|
12328
|
+
unlinkSync3(dbPath);
|
|
11449
12329
|
if (getActiveProfile() === name)
|
|
11450
12330
|
setActiveProfile(null);
|
|
11451
12331
|
return true;
|
|
11452
12332
|
}
|
|
11453
|
-
function
|
|
12333
|
+
function ensureDir22(dir) {
|
|
11454
12334
|
if (!existsSync22(dir)) {
|
|
11455
12335
|
mkdirSync22(dir, { recursive: true });
|
|
11456
12336
|
}
|
|
@@ -12572,7 +13452,7 @@ function writeConfig(config) {
|
|
|
12572
13452
|
}
|
|
12573
13453
|
function getActiveModel() {
|
|
12574
13454
|
const config = readConfig();
|
|
12575
|
-
return config.activeModel ??
|
|
13455
|
+
return config.activeModel ?? DEFAULT_MODEL2;
|
|
12576
13456
|
}
|
|
12577
13457
|
function setActiveModel(modelId) {
|
|
12578
13458
|
const config = readConfig();
|
|
@@ -12646,7 +13526,7 @@ Return JSON with this exact shape:
|
|
|
12646
13526
|
examples: finalExamples,
|
|
12647
13527
|
count: finalExamples.length
|
|
12648
13528
|
};
|
|
12649
|
-
},
|
|
13529
|
+
}, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
|
|
12650
13530
|
var init_dist = __esm(() => {
|
|
12651
13531
|
__defProp2 = Object.defineProperty;
|
|
12652
13532
|
exports_database = {};
|
|
@@ -14082,10 +14962,10 @@ __export(exports_dist2, {
|
|
|
14082
14962
|
acquireLock: () => acquireLock2
|
|
14083
14963
|
});
|
|
14084
14964
|
import { Database as Database3 } from "bun:sqlite";
|
|
14085
|
-
import { mkdirSync as
|
|
14086
|
-
import { join as
|
|
14087
|
-
import { homedir as
|
|
14088
|
-
import { randomUUID as
|
|
14965
|
+
import { mkdirSync as mkdirSync10 } from "fs";
|
|
14966
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
14967
|
+
import { homedir as homedir11 } from "os";
|
|
14968
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
14089
14969
|
import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
|
|
14090
14970
|
import { join as join33 } from "path";
|
|
14091
14971
|
import { homedir as homedir33 } from "os";
|
|
@@ -14099,13 +14979,13 @@ import { homedir as homedir42 } from "os";
|
|
|
14099
14979
|
function getDbPath2() {
|
|
14100
14980
|
if (process.env.CONVERSATIONS_DB_PATH)
|
|
14101
14981
|
return process.env.CONVERSATIONS_DB_PATH;
|
|
14102
|
-
return
|
|
14982
|
+
return join11(homedir11(), ".conversations", "messages.db");
|
|
14103
14983
|
}
|
|
14104
14984
|
function getDb() {
|
|
14105
14985
|
if (db)
|
|
14106
14986
|
return db;
|
|
14107
14987
|
const dbPath = getDbPath2();
|
|
14108
|
-
|
|
14988
|
+
mkdirSync10(dirname3(dbPath), { recursive: true });
|
|
14109
14989
|
db = new Database3(dbPath, { create: true });
|
|
14110
14990
|
db.exec("PRAGMA journal_mode = WAL");
|
|
14111
14991
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -14465,7 +15345,7 @@ function guessMimeType(name) {
|
|
|
14465
15345
|
function sendMessage(opts) {
|
|
14466
15346
|
const db2 = getDb();
|
|
14467
15347
|
const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
|
|
14468
|
-
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${
|
|
15348
|
+
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
|
|
14469
15349
|
const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
|
|
14470
15350
|
const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
|
|
14471
15351
|
const blocking = opts.blocking ? 1 : 0;
|
|
@@ -18507,11 +19387,11 @@ __export(exports_dist3, {
|
|
|
18507
19387
|
AgentNotFoundError: () => AgentNotFoundError2
|
|
18508
19388
|
});
|
|
18509
19389
|
import { Database as Database4 } from "bun:sqlite";
|
|
18510
|
-
import { existsSync as
|
|
18511
|
-
import { dirname as dirname5, join as
|
|
19390
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
|
|
19391
|
+
import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
|
|
18512
19392
|
import { existsSync as existsSync33 } from "fs";
|
|
18513
19393
|
import { join as join34 } from "path";
|
|
18514
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as
|
|
19394
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
18515
19395
|
import { join as join24 } from "path";
|
|
18516
19396
|
import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
|
|
18517
19397
|
import { join as join44 } from "path";
|
|
@@ -18741,8 +19621,8 @@ function isInMemoryDb2(path) {
|
|
|
18741
19621
|
function findNearestTodosDb(startDir) {
|
|
18742
19622
|
let dir = resolve3(startDir);
|
|
18743
19623
|
while (true) {
|
|
18744
|
-
const candidate =
|
|
18745
|
-
if (
|
|
19624
|
+
const candidate = join12(dir, ".todos", "todos.db");
|
|
19625
|
+
if (existsSync7(candidate))
|
|
18746
19626
|
return candidate;
|
|
18747
19627
|
const parent = dirname5(dir);
|
|
18748
19628
|
if (parent === dir)
|
|
@@ -18754,7 +19634,7 @@ function findNearestTodosDb(startDir) {
|
|
|
18754
19634
|
function findGitRoot2(startDir) {
|
|
18755
19635
|
let dir = resolve3(startDir);
|
|
18756
19636
|
while (true) {
|
|
18757
|
-
if (
|
|
19637
|
+
if (existsSync7(join12(dir, ".git")))
|
|
18758
19638
|
return dir;
|
|
18759
19639
|
const parent = dirname5(dir);
|
|
18760
19640
|
if (parent === dir)
|
|
@@ -18774,18 +19654,18 @@ function getDbPath3() {
|
|
|
18774
19654
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
18775
19655
|
const gitRoot = findGitRoot2(cwd);
|
|
18776
19656
|
if (gitRoot) {
|
|
18777
|
-
return
|
|
19657
|
+
return join12(gitRoot, ".todos", "todos.db");
|
|
18778
19658
|
}
|
|
18779
19659
|
}
|
|
18780
19660
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
18781
|
-
return
|
|
19661
|
+
return join12(home, ".todos", "todos.db");
|
|
18782
19662
|
}
|
|
18783
19663
|
function ensureDir3(filePath) {
|
|
18784
19664
|
if (isInMemoryDb2(filePath))
|
|
18785
19665
|
return;
|
|
18786
19666
|
const dir = dirname5(resolve3(filePath));
|
|
18787
|
-
if (!
|
|
18788
|
-
|
|
19667
|
+
if (!existsSync7(dir)) {
|
|
19668
|
+
mkdirSync11(dir, { recursive: true });
|
|
18789
19669
|
}
|
|
18790
19670
|
}
|
|
18791
19671
|
function getDatabase3(dbPath) {
|
|
@@ -19244,14 +20124,14 @@ function ensureProject2(name, path, db2) {
|
|
|
19244
20124
|
}
|
|
19245
20125
|
return createProject3({ name, path }, d);
|
|
19246
20126
|
}
|
|
19247
|
-
function
|
|
20127
|
+
function ensureDir23(dir) {
|
|
19248
20128
|
if (!existsSync23(dir))
|
|
19249
20129
|
mkdirSync24(dir, { recursive: true });
|
|
19250
20130
|
}
|
|
19251
20131
|
function listJsonFiles(dir) {
|
|
19252
20132
|
if (!existsSync23(dir))
|
|
19253
20133
|
return [];
|
|
19254
|
-
return
|
|
20134
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".json"));
|
|
19255
20135
|
}
|
|
19256
20136
|
function readJsonFile(path) {
|
|
19257
20137
|
try {
|
|
@@ -22116,7 +22996,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
|
22116
22996
|
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
22117
22997
|
const dir = getTaskListDir(taskListId);
|
|
22118
22998
|
if (!existsSync43(dir))
|
|
22119
|
-
|
|
22999
|
+
ensureDir23(dir);
|
|
22120
23000
|
const filter = {};
|
|
22121
23001
|
if (projectId)
|
|
22122
23002
|
filter["project_id"] = projectId;
|
|
@@ -22334,7 +23214,7 @@ function metadataKey(agent) {
|
|
|
22334
23214
|
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
22335
23215
|
const dir = getTaskListDir2(agent, taskListId);
|
|
22336
23216
|
if (!existsSync52(dir))
|
|
22337
|
-
|
|
23217
|
+
ensureDir23(dir);
|
|
22338
23218
|
const filter = {};
|
|
22339
23219
|
if (projectId)
|
|
22340
23220
|
filter["project_id"] = projectId;
|
|
@@ -23862,9 +24742,9 @@ __export(exports_dist4, {
|
|
|
23862
24742
|
CATEGORIES: () => CATEGORIES,
|
|
23863
24743
|
AGENT_TARGETS: () => AGENT_TARGETS
|
|
23864
24744
|
});
|
|
23865
|
-
import { existsSync as
|
|
23866
|
-
import { join as
|
|
23867
|
-
import { homedir as
|
|
24745
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
|
|
24746
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
24747
|
+
import { homedir as homedir12 } from "os";
|
|
23868
24748
|
import { fileURLToPath } from "url";
|
|
23869
24749
|
import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
|
|
23870
24750
|
import { join as join25 } from "path";
|
|
@@ -23975,35 +24855,35 @@ function normalizeSkillName(name) {
|
|
|
23975
24855
|
function findSkillsDir() {
|
|
23976
24856
|
let dir = __dirname2;
|
|
23977
24857
|
for (let i = 0;i < 5; i++) {
|
|
23978
|
-
const candidate =
|
|
23979
|
-
if (
|
|
24858
|
+
const candidate = join13(dir, "skills");
|
|
24859
|
+
if (existsSync8(candidate)) {
|
|
23980
24860
|
return candidate;
|
|
23981
24861
|
}
|
|
23982
24862
|
dir = dirname6(dir);
|
|
23983
24863
|
}
|
|
23984
|
-
return
|
|
24864
|
+
return join13(__dirname2, "..", "skills");
|
|
23985
24865
|
}
|
|
23986
24866
|
function getSkillPath(name) {
|
|
23987
24867
|
const skillName = normalizeSkillName(name);
|
|
23988
|
-
return
|
|
24868
|
+
return join13(SKILLS_DIR, skillName);
|
|
23989
24869
|
}
|
|
23990
24870
|
function skillExists(name) {
|
|
23991
|
-
return
|
|
24871
|
+
return existsSync8(getSkillPath(name));
|
|
23992
24872
|
}
|
|
23993
24873
|
function installSkill(name, options = {}) {
|
|
23994
24874
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
23995
24875
|
const skillName = normalizeSkillName(name);
|
|
23996
24876
|
const sourcePath = getSkillPath(name);
|
|
23997
|
-
const destDir =
|
|
23998
|
-
const destPath =
|
|
23999
|
-
if (!
|
|
24877
|
+
const destDir = join13(targetDir, ".skills");
|
|
24878
|
+
const destPath = join13(destDir, skillName);
|
|
24879
|
+
if (!existsSync8(sourcePath)) {
|
|
24000
24880
|
return {
|
|
24001
24881
|
skill: name,
|
|
24002
24882
|
success: false,
|
|
24003
24883
|
error: `Skill '${name}' not found`
|
|
24004
24884
|
};
|
|
24005
24885
|
}
|
|
24006
|
-
if (
|
|
24886
|
+
if (existsSync8(destPath) && !overwrite) {
|
|
24007
24887
|
return {
|
|
24008
24888
|
skill: name,
|
|
24009
24889
|
success: false,
|
|
@@ -24012,10 +24892,10 @@ function installSkill(name, options = {}) {
|
|
|
24012
24892
|
};
|
|
24013
24893
|
}
|
|
24014
24894
|
try {
|
|
24015
|
-
if (!
|
|
24016
|
-
|
|
24895
|
+
if (!existsSync8(destDir)) {
|
|
24896
|
+
mkdirSync12(destDir, { recursive: true });
|
|
24017
24897
|
}
|
|
24018
|
-
if (
|
|
24898
|
+
if (existsSync8(destPath) && overwrite) {
|
|
24019
24899
|
rmSync2(destPath, { recursive: true, force: true });
|
|
24020
24900
|
}
|
|
24021
24901
|
cpSync(sourcePath, destPath, {
|
|
@@ -24054,10 +24934,10 @@ function installSkills(names, options = {}) {
|
|
|
24054
24934
|
return names.map((name) => installSkill(name, options));
|
|
24055
24935
|
}
|
|
24056
24936
|
function updateSkillsIndex(skillsDir) {
|
|
24057
|
-
const indexPath =
|
|
24937
|
+
const indexPath = join13(skillsDir, "index.ts");
|
|
24058
24938
|
const meta = loadMeta(skillsDir);
|
|
24059
24939
|
const disabledSet = new Set(meta.disabled || []);
|
|
24060
|
-
const skills =
|
|
24940
|
+
const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
|
|
24061
24941
|
const exports = skills.map((s) => {
|
|
24062
24942
|
const name = s.replace("skill-", "").replace(/-/g, "_");
|
|
24063
24943
|
return `export * as ${name} from './${s}/src/index.js';`;
|
|
@@ -24073,11 +24953,11 @@ ${exports}
|
|
|
24073
24953
|
writeFileSync6(indexPath, content);
|
|
24074
24954
|
}
|
|
24075
24955
|
function getMetaPath(skillsDir) {
|
|
24076
|
-
return
|
|
24956
|
+
return join13(skillsDir, ".meta.json");
|
|
24077
24957
|
}
|
|
24078
24958
|
function loadMeta(skillsDir) {
|
|
24079
24959
|
const metaPath2 = getMetaPath(skillsDir);
|
|
24080
|
-
if (
|
|
24960
|
+
if (existsSync8(metaPath2)) {
|
|
24081
24961
|
try {
|
|
24082
24962
|
return JSON.parse(readFileSync7(metaPath2, "utf-8"));
|
|
24083
24963
|
} catch {}
|
|
@@ -24092,8 +24972,8 @@ function recordInstall(skillsDir, name) {
|
|
|
24092
24972
|
const skillName = normalizeSkillName(name);
|
|
24093
24973
|
let version = "unknown";
|
|
24094
24974
|
try {
|
|
24095
|
-
const pkgPath =
|
|
24096
|
-
if (
|
|
24975
|
+
const pkgPath = join13(skillsDir, skillName, "package.json");
|
|
24976
|
+
if (existsSync8(pkgPath)) {
|
|
24097
24977
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
24098
24978
|
version = pkg.version || "unknown";
|
|
24099
24979
|
}
|
|
@@ -24107,12 +24987,12 @@ function recordRemove(skillsDir, name) {
|
|
|
24107
24987
|
saveMeta(skillsDir, meta);
|
|
24108
24988
|
}
|
|
24109
24989
|
function getInstallMeta(targetDir = process.cwd()) {
|
|
24110
|
-
return loadMeta(
|
|
24990
|
+
return loadMeta(join13(targetDir, ".skills"));
|
|
24111
24991
|
}
|
|
24112
24992
|
function disableSkill(name, targetDir = process.cwd()) {
|
|
24113
|
-
const skillsDir =
|
|
24993
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
24114
24994
|
const skillName = normalizeSkillName(name);
|
|
24115
|
-
if (!
|
|
24995
|
+
if (!existsSync8(join13(skillsDir, skillName)))
|
|
24116
24996
|
return false;
|
|
24117
24997
|
const meta = loadMeta(skillsDir);
|
|
24118
24998
|
const disabled = new Set(meta.disabled || []);
|
|
@@ -24125,7 +25005,7 @@ function disableSkill(name, targetDir = process.cwd()) {
|
|
|
24125
25005
|
return true;
|
|
24126
25006
|
}
|
|
24127
25007
|
function enableSkill(name, targetDir = process.cwd()) {
|
|
24128
|
-
const skillsDir =
|
|
25008
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
24129
25009
|
const meta = loadMeta(skillsDir);
|
|
24130
25010
|
const disabled = new Set(meta.disabled || []);
|
|
24131
25011
|
if (!disabled.has(name))
|
|
@@ -24137,24 +25017,24 @@ function enableSkill(name, targetDir = process.cwd()) {
|
|
|
24137
25017
|
return true;
|
|
24138
25018
|
}
|
|
24139
25019
|
function getDisabledSkills(targetDir = process.cwd()) {
|
|
24140
|
-
const meta = loadMeta(
|
|
25020
|
+
const meta = loadMeta(join13(targetDir, ".skills"));
|
|
24141
25021
|
return meta.disabled || [];
|
|
24142
25022
|
}
|
|
24143
25023
|
function getInstalledSkills(targetDir = process.cwd()) {
|
|
24144
|
-
const skillsDir =
|
|
24145
|
-
if (!
|
|
25024
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
25025
|
+
if (!existsSync8(skillsDir)) {
|
|
24146
25026
|
return [];
|
|
24147
25027
|
}
|
|
24148
|
-
return
|
|
24149
|
-
const fullPath =
|
|
25028
|
+
return readdirSync6(skillsDir).filter((f) => {
|
|
25029
|
+
const fullPath = join13(skillsDir, f);
|
|
24150
25030
|
return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
|
|
24151
25031
|
}).map((f) => f.replace("skill-", ""));
|
|
24152
25032
|
}
|
|
24153
25033
|
function removeSkill(name, targetDir = process.cwd()) {
|
|
24154
25034
|
const skillName = normalizeSkillName(name);
|
|
24155
|
-
const skillsDir =
|
|
24156
|
-
const skillPath =
|
|
24157
|
-
if (!
|
|
25035
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
25036
|
+
const skillPath = join13(skillsDir, skillName);
|
|
25037
|
+
if (!existsSync8(skillPath)) {
|
|
24158
25038
|
return false;
|
|
24159
25039
|
}
|
|
24160
25040
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
@@ -24165,24 +25045,24 @@ function removeSkill(name, targetDir = process.cwd()) {
|
|
|
24165
25045
|
function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
24166
25046
|
const agentDir = `.${agent}`;
|
|
24167
25047
|
if (scope === "project") {
|
|
24168
|
-
return
|
|
25048
|
+
return join13(projectDir || process.cwd(), agentDir, "skills");
|
|
24169
25049
|
}
|
|
24170
|
-
return
|
|
25050
|
+
return join13(homedir12(), agentDir, "skills");
|
|
24171
25051
|
}
|
|
24172
25052
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
24173
25053
|
const skillName = normalizeSkillName(name);
|
|
24174
|
-
return
|
|
25054
|
+
return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
24175
25055
|
}
|
|
24176
25056
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
24177
25057
|
const { agent, scope = "global", projectDir } = options;
|
|
24178
25058
|
const skillName = normalizeSkillName(name);
|
|
24179
25059
|
const sourcePath = getSkillPath(name);
|
|
24180
|
-
if (!
|
|
25060
|
+
if (!existsSync8(sourcePath)) {
|
|
24181
25061
|
return { skill: name, success: false, error: `Skill '${name}' not found` };
|
|
24182
25062
|
}
|
|
24183
25063
|
let skillMdContent = null;
|
|
24184
|
-
const skillMdPath =
|
|
24185
|
-
if (
|
|
25064
|
+
const skillMdPath = join13(sourcePath, "SKILL.md");
|
|
25065
|
+
if (existsSync8(skillMdPath)) {
|
|
24186
25066
|
skillMdContent = readFileSync7(skillMdPath, "utf-8");
|
|
24187
25067
|
} else if (generateSkillMd) {
|
|
24188
25068
|
skillMdContent = generateSkillMd(name);
|
|
@@ -24192,8 +25072,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24192
25072
|
}
|
|
24193
25073
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
24194
25074
|
if (scope === "global") {
|
|
24195
|
-
const agentBaseDir2 =
|
|
24196
|
-
if (!
|
|
25075
|
+
const agentBaseDir2 = join13(homedir12(), `.${agent}`);
|
|
25076
|
+
if (!existsSync8(agentBaseDir2)) {
|
|
24197
25077
|
const agentLabels = {
|
|
24198
25078
|
claude: "Claude Code",
|
|
24199
25079
|
codex: "Codex CLI",
|
|
@@ -24216,8 +25096,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24216
25096
|
}
|
|
24217
25097
|
}
|
|
24218
25098
|
try {
|
|
24219
|
-
|
|
24220
|
-
writeFileSync6(
|
|
25099
|
+
mkdirSync12(destDir, { recursive: true });
|
|
25100
|
+
writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
|
|
24221
25101
|
return { skill: name, success: true, path: destDir };
|
|
24222
25102
|
} catch (error) {
|
|
24223
25103
|
return {
|
|
@@ -24230,7 +25110,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24230
25110
|
function removeSkillForAgent(name, options) {
|
|
24231
25111
|
const { agent, scope = "global", projectDir } = options;
|
|
24232
25112
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
24233
|
-
if (!
|
|
25113
|
+
if (!existsSync8(destDir)) {
|
|
24234
25114
|
return false;
|
|
24235
25115
|
}
|
|
24236
25116
|
rmSync2(destDir, { recursive: true, force: true });
|
|
@@ -26154,7 +27034,7 @@ __export(exports_cron_manager, {
|
|
|
26154
27034
|
deleteCronJob: () => deleteCronJob,
|
|
26155
27035
|
createCronJob: () => createCronJob
|
|
26156
27036
|
});
|
|
26157
|
-
import { randomUUID as
|
|
27037
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
26158
27038
|
function ensureCronTable() {
|
|
26159
27039
|
const db2 = getDatabase();
|
|
26160
27040
|
db2.exec(`
|
|
@@ -26183,7 +27063,7 @@ function ensureCronTable() {
|
|
|
26183
27063
|
function createCronJob(schedule, task, name) {
|
|
26184
27064
|
ensureCronTable();
|
|
26185
27065
|
const db2 = getDatabase();
|
|
26186
|
-
const id =
|
|
27066
|
+
const id = randomUUID12();
|
|
26187
27067
|
db2.prepare(`
|
|
26188
27068
|
INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
|
|
26189
27069
|
VALUES (?, ?, ?, ?, 1)
|
|
@@ -26241,7 +27121,7 @@ function getCronEvents(jobId, limit = 10) {
|
|
|
26241
27121
|
async function executeCronJob(job) {
|
|
26242
27122
|
ensureCronTable();
|
|
26243
27123
|
const db2 = getDatabase();
|
|
26244
|
-
const eventId =
|
|
27124
|
+
const eventId = randomUUID12();
|
|
26245
27125
|
const startedAt = new Date().toISOString();
|
|
26246
27126
|
db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
|
|
26247
27127
|
try {
|
|
@@ -26332,7 +27212,7 @@ __export(exports_url_watcher, {
|
|
|
26332
27212
|
deleteWatchJob: () => deleteWatchJob,
|
|
26333
27213
|
createWatchJob: () => createWatchJob
|
|
26334
27214
|
});
|
|
26335
|
-
import { randomUUID as
|
|
27215
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
26336
27216
|
import { createHash } from "crypto";
|
|
26337
27217
|
function ensureWatchTables() {
|
|
26338
27218
|
const db2 = getDatabase();
|
|
@@ -26364,7 +27244,7 @@ function ensureWatchTables() {
|
|
|
26364
27244
|
function createWatchJob(url, schedule, opts) {
|
|
26365
27245
|
ensureWatchTables();
|
|
26366
27246
|
const db2 = getDatabase();
|
|
26367
|
-
const id =
|
|
27247
|
+
const id = randomUUID13();
|
|
26368
27248
|
db2.prepare(`
|
|
26369
27249
|
INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
|
|
26370
27250
|
VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
@@ -26400,7 +27280,7 @@ function getWatchEvents(watchId, limit = 20) {
|
|
|
26400
27280
|
async function checkWatchJob(job) {
|
|
26401
27281
|
ensureWatchTables();
|
|
26402
27282
|
const db2 = getDatabase();
|
|
26403
|
-
const eventId =
|
|
27283
|
+
const eventId = randomUUID13();
|
|
26404
27284
|
const checkedAt = new Date().toISOString();
|
|
26405
27285
|
let newContent = "";
|
|
26406
27286
|
try {
|
|
@@ -30552,23 +31432,23 @@ var NEVER = INVALID;
|
|
|
30552
31432
|
init_session();
|
|
30553
31433
|
init_actions();
|
|
30554
31434
|
import { readFileSync as readFileSync8 } from "fs";
|
|
30555
|
-
import { join as
|
|
31435
|
+
import { join as join14 } from "path";
|
|
30556
31436
|
|
|
30557
31437
|
// src/lib/screenshot.ts
|
|
30558
31438
|
init_types();
|
|
30559
31439
|
init_gallery();
|
|
30560
31440
|
var import_sharp = __toESM(require_lib(), 1);
|
|
30561
|
-
import { join as
|
|
30562
|
-
import { mkdirSync as
|
|
30563
|
-
import { homedir as
|
|
31441
|
+
import { join as join4 } from "path";
|
|
31442
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
31443
|
+
import { homedir as homedir4 } from "os";
|
|
30564
31444
|
function getDataDir2() {
|
|
30565
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
31445
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
30566
31446
|
}
|
|
30567
31447
|
function getScreenshotDir(projectId) {
|
|
30568
|
-
const base =
|
|
31448
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
30569
31449
|
const date = new Date().toISOString().split("T")[0];
|
|
30570
|
-
const dir = projectId ?
|
|
30571
|
-
|
|
31450
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
31451
|
+
mkdirSync4(dir, { recursive: true });
|
|
30572
31452
|
return dir;
|
|
30573
31453
|
}
|
|
30574
31454
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -30583,7 +31463,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
30583
31463
|
}
|
|
30584
31464
|
}
|
|
30585
31465
|
async function generateThumbnail(raw, dir, stem) {
|
|
30586
|
-
const thumbPath =
|
|
31466
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
30587
31467
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
30588
31468
|
await Bun.write(thumbPath, thumbBuffer);
|
|
30589
31469
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -30640,7 +31520,7 @@ async function takeScreenshot(page, opts) {
|
|
|
30640
31520
|
const compressedSizeBytes = finalBuffer.length;
|
|
30641
31521
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
30642
31522
|
const ext = format;
|
|
30643
|
-
const screenshotPath = opts?.path ??
|
|
31523
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
30644
31524
|
await Bun.write(screenshotPath, finalBuffer);
|
|
30645
31525
|
let thumbnailPath;
|
|
30646
31526
|
let thumbnailBase64;
|
|
@@ -30700,12 +31580,12 @@ async function takeScreenshot(page, opts) {
|
|
|
30700
31580
|
}
|
|
30701
31581
|
async function generatePDF(page, opts) {
|
|
30702
31582
|
try {
|
|
30703
|
-
const base =
|
|
31583
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
30704
31584
|
const date = new Date().toISOString().split("T")[0];
|
|
30705
|
-
const dir = opts?.projectId ?
|
|
30706
|
-
|
|
31585
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
31586
|
+
mkdirSync4(dir, { recursive: true });
|
|
30707
31587
|
const timestamp = Date.now();
|
|
30708
|
-
const pdfPath = opts?.path ??
|
|
31588
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
30709
31589
|
const buffer = await page.pdf({
|
|
30710
31590
|
path: pdfPath,
|
|
30711
31591
|
format: opts?.format ?? "A4",
|
|
@@ -30726,118 +31606,8 @@ async function generatePDF(page, opts) {
|
|
|
30726
31606
|
// src/mcp/index.ts
|
|
30727
31607
|
init_network();
|
|
30728
31608
|
|
|
30729
|
-
// src/engines/cdp.ts
|
|
30730
|
-
init_types();
|
|
30731
|
-
|
|
30732
|
-
class CDPClient {
|
|
30733
|
-
session;
|
|
30734
|
-
networkEnabled = false;
|
|
30735
|
-
performanceEnabled = false;
|
|
30736
|
-
constructor(session) {
|
|
30737
|
-
this.session = session;
|
|
30738
|
-
}
|
|
30739
|
-
static async fromPage(page) {
|
|
30740
|
-
try {
|
|
30741
|
-
const session = await page.context().newCDPSession(page);
|
|
30742
|
-
return new CDPClient(session);
|
|
30743
|
-
} catch (err) {
|
|
30744
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
30745
|
-
}
|
|
30746
|
-
}
|
|
30747
|
-
async send(method, params) {
|
|
30748
|
-
try {
|
|
30749
|
-
return await this.session.send(method, params);
|
|
30750
|
-
} catch (err) {
|
|
30751
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
30752
|
-
}
|
|
30753
|
-
}
|
|
30754
|
-
on(event, handler) {
|
|
30755
|
-
this.session.on(event, handler);
|
|
30756
|
-
}
|
|
30757
|
-
off(event, handler) {
|
|
30758
|
-
this.session.off(event, handler);
|
|
30759
|
-
}
|
|
30760
|
-
async enableNetwork() {
|
|
30761
|
-
if (!this.networkEnabled) {
|
|
30762
|
-
await this.send("Network.enable");
|
|
30763
|
-
this.networkEnabled = true;
|
|
30764
|
-
}
|
|
30765
|
-
}
|
|
30766
|
-
async enablePerformance() {
|
|
30767
|
-
if (!this.performanceEnabled) {
|
|
30768
|
-
await this.send("Performance.enable");
|
|
30769
|
-
this.performanceEnabled = true;
|
|
30770
|
-
}
|
|
30771
|
-
}
|
|
30772
|
-
async getPerformanceMetrics() {
|
|
30773
|
-
await this.enablePerformance();
|
|
30774
|
-
const result = await this.send("Performance.getMetrics");
|
|
30775
|
-
const m = {};
|
|
30776
|
-
for (const metric of result.metrics) {
|
|
30777
|
-
m[metric.name] = metric.value;
|
|
30778
|
-
}
|
|
30779
|
-
return {
|
|
30780
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
30781
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
30782
|
-
dom_interactive: m["DOMInteractive"],
|
|
30783
|
-
dom_complete: m["DOMComplete"],
|
|
30784
|
-
load_event: m["LoadEventEnd"]
|
|
30785
|
-
};
|
|
30786
|
-
}
|
|
30787
|
-
async startJSCoverage() {
|
|
30788
|
-
await this.send("Profiler.enable");
|
|
30789
|
-
await this.send("Debugger.enable");
|
|
30790
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
30791
|
-
callCount: false,
|
|
30792
|
-
detailed: true
|
|
30793
|
-
});
|
|
30794
|
-
}
|
|
30795
|
-
async stopJSCoverage() {
|
|
30796
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
30797
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
30798
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
30799
|
-
url: r.url,
|
|
30800
|
-
text: "",
|
|
30801
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
30802
|
-
}));
|
|
30803
|
-
}
|
|
30804
|
-
async getCoverage() {
|
|
30805
|
-
await this.startJSCoverage();
|
|
30806
|
-
const js = await this.stopJSCoverage();
|
|
30807
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
30808
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
30809
|
-
}
|
|
30810
|
-
async captureHAREntries(page, handler) {
|
|
30811
|
-
await this.enableNetwork();
|
|
30812
|
-
const requestTimings = new Map;
|
|
30813
|
-
const onRequest = (params) => {
|
|
30814
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
30815
|
-
};
|
|
30816
|
-
const onResponse = (params) => {
|
|
30817
|
-
const start = requestTimings.get(params.requestId);
|
|
30818
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
30819
|
-
handler({
|
|
30820
|
-
method: "GET",
|
|
30821
|
-
url: params.response.url,
|
|
30822
|
-
status: params.response.status,
|
|
30823
|
-
duration
|
|
30824
|
-
});
|
|
30825
|
-
};
|
|
30826
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
30827
|
-
this.on("Network.responseReceived", onResponse);
|
|
30828
|
-
return () => {
|
|
30829
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
30830
|
-
this.off("Network.responseReceived", onResponse);
|
|
30831
|
-
};
|
|
30832
|
-
}
|
|
30833
|
-
async detach() {
|
|
30834
|
-
try {
|
|
30835
|
-
await this.session.detach();
|
|
30836
|
-
} catch {}
|
|
30837
|
-
}
|
|
30838
|
-
}
|
|
30839
|
-
|
|
30840
31609
|
// src/lib/performance.ts
|
|
31610
|
+
init_cdp();
|
|
30841
31611
|
async function getPerformanceMetrics(page) {
|
|
30842
31612
|
const navTiming = await page.evaluate(() => {
|
|
30843
31613
|
const t = performance.timing;
|
|
@@ -30929,144 +31699,8 @@ async function setSessionStorage(page, key, value) {
|
|
|
30929
31699
|
await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
|
|
30930
31700
|
}
|
|
30931
31701
|
|
|
30932
|
-
// src/
|
|
30933
|
-
|
|
30934
|
-
init_types();
|
|
30935
|
-
import { randomUUID as randomUUID5 } from "crypto";
|
|
30936
|
-
function deserialize2(row) {
|
|
30937
|
-
return {
|
|
30938
|
-
...row,
|
|
30939
|
-
project_id: row.project_id ?? undefined,
|
|
30940
|
-
start_url: row.start_url ?? undefined,
|
|
30941
|
-
steps: JSON.parse(row.steps)
|
|
30942
|
-
};
|
|
30943
|
-
}
|
|
30944
|
-
function createRecording(data) {
|
|
30945
|
-
const db = getDatabase();
|
|
30946
|
-
const id = randomUUID5();
|
|
30947
|
-
db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
|
|
30948
|
-
return getRecording(id);
|
|
30949
|
-
}
|
|
30950
|
-
function getRecording(id) {
|
|
30951
|
-
const db = getDatabase();
|
|
30952
|
-
const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
|
|
30953
|
-
if (!row)
|
|
30954
|
-
throw new RecordingNotFoundError(id);
|
|
30955
|
-
return deserialize2(row);
|
|
30956
|
-
}
|
|
30957
|
-
function listRecordings(projectId) {
|
|
30958
|
-
const db = getDatabase();
|
|
30959
|
-
const rows = projectId ? db.query("SELECT * FROM recordings WHERE project_id = ? ORDER BY created_at DESC").all(projectId) : db.query("SELECT * FROM recordings ORDER BY created_at DESC").all();
|
|
30960
|
-
return rows.map(deserialize2);
|
|
30961
|
-
}
|
|
30962
|
-
function updateRecording(id, data) {
|
|
30963
|
-
const db = getDatabase();
|
|
30964
|
-
const fields = [];
|
|
30965
|
-
const values = [];
|
|
30966
|
-
if (data.name !== undefined) {
|
|
30967
|
-
fields.push("name = ?");
|
|
30968
|
-
values.push(data.name);
|
|
30969
|
-
}
|
|
30970
|
-
if (data.steps !== undefined) {
|
|
30971
|
-
fields.push("steps = ?");
|
|
30972
|
-
values.push(JSON.stringify(data.steps));
|
|
30973
|
-
}
|
|
30974
|
-
if (data.start_url !== undefined) {
|
|
30975
|
-
fields.push("start_url = ?");
|
|
30976
|
-
values.push(data.start_url ?? null);
|
|
30977
|
-
}
|
|
30978
|
-
if (fields.length === 0)
|
|
30979
|
-
return getRecording(id);
|
|
30980
|
-
values.push(id);
|
|
30981
|
-
db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
30982
|
-
return getRecording(id);
|
|
30983
|
-
}
|
|
30984
|
-
|
|
30985
|
-
// src/lib/recorder.ts
|
|
30986
|
-
init_actions();
|
|
30987
|
-
init_types();
|
|
30988
|
-
var activeRecordings = new Map;
|
|
30989
|
-
function startRecording(sessionId, name, startUrl) {
|
|
30990
|
-
const steps = [];
|
|
30991
|
-
const recording = createRecording({ name, start_url: startUrl, steps });
|
|
30992
|
-
activeRecordings.set(recording.id, {
|
|
30993
|
-
id: recording.id,
|
|
30994
|
-
steps,
|
|
30995
|
-
cleanup: () => {}
|
|
30996
|
-
});
|
|
30997
|
-
return recording;
|
|
30998
|
-
}
|
|
30999
|
-
function recordStep(recordingId, step) {
|
|
31000
|
-
const active = activeRecordings.get(recordingId);
|
|
31001
|
-
if (!active)
|
|
31002
|
-
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
31003
|
-
active.steps.push({ ...step, timestamp: Date.now() });
|
|
31004
|
-
}
|
|
31005
|
-
function stopRecording(recordingId) {
|
|
31006
|
-
const active = activeRecordings.get(recordingId);
|
|
31007
|
-
if (!active)
|
|
31008
|
-
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
31009
|
-
active.cleanup();
|
|
31010
|
-
activeRecordings.delete(recordingId);
|
|
31011
|
-
return updateRecording(recordingId, { steps: active.steps });
|
|
31012
|
-
}
|
|
31013
|
-
async function replayRecording(recordingId, page) {
|
|
31014
|
-
const recording = getRecording(recordingId);
|
|
31015
|
-
const startTime = Date.now();
|
|
31016
|
-
let executed = 0;
|
|
31017
|
-
let failed = 0;
|
|
31018
|
-
const errors2 = [];
|
|
31019
|
-
for (const step of recording.steps) {
|
|
31020
|
-
try {
|
|
31021
|
-
switch (step.type) {
|
|
31022
|
-
case "navigate":
|
|
31023
|
-
if (step.url)
|
|
31024
|
-
await navigate(page, step.url);
|
|
31025
|
-
break;
|
|
31026
|
-
case "click":
|
|
31027
|
-
if (step.selector)
|
|
31028
|
-
await click(page, step.selector);
|
|
31029
|
-
break;
|
|
31030
|
-
case "type":
|
|
31031
|
-
if (step.selector && step.value)
|
|
31032
|
-
await type(page, step.selector, step.value);
|
|
31033
|
-
break;
|
|
31034
|
-
case "scroll":
|
|
31035
|
-
await scroll(page, "down");
|
|
31036
|
-
break;
|
|
31037
|
-
case "hover":
|
|
31038
|
-
if (step.selector) {
|
|
31039
|
-
const el = await page.$(step.selector);
|
|
31040
|
-
if (el)
|
|
31041
|
-
await el.hover();
|
|
31042
|
-
}
|
|
31043
|
-
break;
|
|
31044
|
-
case "evaluate":
|
|
31045
|
-
if (step.value)
|
|
31046
|
-
await page.evaluate(step.value);
|
|
31047
|
-
break;
|
|
31048
|
-
case "wait":
|
|
31049
|
-
if (step.selector) {
|
|
31050
|
-
await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
|
|
31051
|
-
}
|
|
31052
|
-
break;
|
|
31053
|
-
}
|
|
31054
|
-
executed++;
|
|
31055
|
-
} catch (err) {
|
|
31056
|
-
failed++;
|
|
31057
|
-
errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
31058
|
-
}
|
|
31059
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
31060
|
-
}
|
|
31061
|
-
return {
|
|
31062
|
-
recording_id: recordingId,
|
|
31063
|
-
success: failed === 0,
|
|
31064
|
-
steps_executed: executed,
|
|
31065
|
-
steps_failed: failed,
|
|
31066
|
-
errors: errors2,
|
|
31067
|
-
duration_ms: Date.now() - startTime
|
|
31068
|
-
};
|
|
31069
|
-
}
|
|
31702
|
+
// src/mcp/index.ts
|
|
31703
|
+
init_recorder();
|
|
31070
31704
|
|
|
31071
31705
|
// src/lib/crawler.ts
|
|
31072
31706
|
init_types();
|
|
@@ -31249,16 +31883,16 @@ init_gallery();
|
|
|
31249
31883
|
|
|
31250
31884
|
// src/lib/downloads.ts
|
|
31251
31885
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
31252
|
-
import { join as
|
|
31253
|
-
import { mkdirSync as
|
|
31254
|
-
import { homedir as
|
|
31886
|
+
import { join as join5, basename, extname } from "path";
|
|
31887
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
31888
|
+
import { homedir as homedir5 } from "os";
|
|
31255
31889
|
function getDataDir3() {
|
|
31256
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
31890
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
31257
31891
|
}
|
|
31258
31892
|
function getDownloadsDir(sessionId) {
|
|
31259
|
-
const base =
|
|
31260
|
-
const dir = sessionId ?
|
|
31261
|
-
|
|
31893
|
+
const base = join5(getDataDir3(), "downloads");
|
|
31894
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
31895
|
+
mkdirSync5(dir, { recursive: true });
|
|
31262
31896
|
return dir;
|
|
31263
31897
|
}
|
|
31264
31898
|
function metaPath(filePath) {
|
|
@@ -31270,7 +31904,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
31270
31904
|
const ext = extname(filename) || "";
|
|
31271
31905
|
const stem = basename(filename, ext);
|
|
31272
31906
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
31273
|
-
const filePath =
|
|
31907
|
+
const filePath = join5(dir, uniqueName);
|
|
31274
31908
|
writeFileSync(filePath, buffer);
|
|
31275
31909
|
const meta = {
|
|
31276
31910
|
id,
|
|
@@ -31299,20 +31933,20 @@ function listDownloads(sessionId) {
|
|
|
31299
31933
|
const dir = getDownloadsDir(sessionId);
|
|
31300
31934
|
const results = [];
|
|
31301
31935
|
function scanDir(d) {
|
|
31302
|
-
if (!
|
|
31936
|
+
if (!existsSync2(d))
|
|
31303
31937
|
return;
|
|
31304
|
-
const entries =
|
|
31938
|
+
const entries = readdirSync2(d);
|
|
31305
31939
|
for (const entry of entries) {
|
|
31306
31940
|
if (entry.endsWith(".meta.json"))
|
|
31307
31941
|
continue;
|
|
31308
|
-
const full =
|
|
31942
|
+
const full = join5(d, entry);
|
|
31309
31943
|
const stat = statSync(full);
|
|
31310
31944
|
if (stat.isDirectory()) {
|
|
31311
31945
|
scanDir(full);
|
|
31312
31946
|
continue;
|
|
31313
31947
|
}
|
|
31314
31948
|
const mpath = metaPath(full);
|
|
31315
|
-
if (!
|
|
31949
|
+
if (!existsSync2(mpath))
|
|
31316
31950
|
continue;
|
|
31317
31951
|
try {
|
|
31318
31952
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -31342,9 +31976,9 @@ function deleteDownload(id, sessionId) {
|
|
|
31342
31976
|
if (!file)
|
|
31343
31977
|
return false;
|
|
31344
31978
|
try {
|
|
31345
|
-
|
|
31346
|
-
if (
|
|
31347
|
-
|
|
31979
|
+
unlinkSync2(file.path);
|
|
31980
|
+
if (existsSync2(file.meta_path))
|
|
31981
|
+
unlinkSync2(file.meta_path);
|
|
31348
31982
|
return true;
|
|
31349
31983
|
} catch {
|
|
31350
31984
|
return false;
|
|
@@ -31390,9 +32024,9 @@ function detectType(filename) {
|
|
|
31390
32024
|
|
|
31391
32025
|
// src/lib/gallery-diff.ts
|
|
31392
32026
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
31393
|
-
import { join as
|
|
31394
|
-
import { mkdirSync as
|
|
31395
|
-
import { homedir as
|
|
32027
|
+
import { join as join6 } from "path";
|
|
32028
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
32029
|
+
import { homedir as homedir6 } from "os";
|
|
31396
32030
|
async function diffImages(path1, path2) {
|
|
31397
32031
|
const img1 = import_sharp2.default(path1);
|
|
31398
32032
|
const img2 = import_sharp2.default(path2);
|
|
@@ -31423,10 +32057,10 @@ async function diffImages(path1, path2) {
|
|
|
31423
32057
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
31424
32058
|
}
|
|
31425
32059
|
}
|
|
31426
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
31427
|
-
const diffDir =
|
|
31428
|
-
|
|
31429
|
-
const diffPath =
|
|
32060
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
32061
|
+
const diffDir = join6(dataDir, "diffs");
|
|
32062
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
32063
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
31430
32064
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
31431
32065
|
await Bun.write(diffPath, diffImageBuffer);
|
|
31432
32066
|
return {
|
|
@@ -31442,9 +32076,9 @@ async function diffImages(path1, path2) {
|
|
|
31442
32076
|
init_snapshot();
|
|
31443
32077
|
|
|
31444
32078
|
// src/lib/files-integration.ts
|
|
31445
|
-
import { join as
|
|
31446
|
-
import { mkdirSync as
|
|
31447
|
-
import { homedir as
|
|
32079
|
+
import { join as join7 } from "path";
|
|
32080
|
+
import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
|
|
32081
|
+
import { homedir as homedir7 } from "os";
|
|
31448
32082
|
async function persistFile(localPath, opts) {
|
|
31449
32083
|
try {
|
|
31450
32084
|
const mod = await import("@hasna/files");
|
|
@@ -31453,12 +32087,12 @@ async function persistFile(localPath, opts) {
|
|
|
31453
32087
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
31454
32088
|
}
|
|
31455
32089
|
} catch {}
|
|
31456
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
32090
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
31457
32091
|
const date = new Date().toISOString().split("T")[0];
|
|
31458
|
-
const dir =
|
|
31459
|
-
|
|
32092
|
+
const dir = join7(dataDir, "persistent", date);
|
|
32093
|
+
mkdirSync7(dir, { recursive: true });
|
|
31460
32094
|
const filename = localPath.split("/").pop() ?? "file";
|
|
31461
|
-
const targetPath =
|
|
32095
|
+
const targetPath = join7(dir, filename);
|
|
31462
32096
|
copyFileSync2(localPath, targetPath);
|
|
31463
32097
|
return {
|
|
31464
32098
|
id: `local-${Date.now()}`,
|
|
@@ -31468,6 +32102,9 @@ async function persistFile(localPath, opts) {
|
|
|
31468
32102
|
};
|
|
31469
32103
|
}
|
|
31470
32104
|
|
|
32105
|
+
// src/mcp/index.ts
|
|
32106
|
+
init_recordings();
|
|
32107
|
+
|
|
31471
32108
|
// src/db/timeline.ts
|
|
31472
32109
|
init_schema();
|
|
31473
32110
|
function logEvent(sessionId, eventType, details = {}) {
|
|
@@ -31565,7 +32202,7 @@ async function closeTab(page, index) {
|
|
|
31565
32202
|
init_dialogs();
|
|
31566
32203
|
init_profiles();
|
|
31567
32204
|
init_types();
|
|
31568
|
-
var _pkg = JSON.parse(readFileSync8(
|
|
32205
|
+
var _pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
|
|
31569
32206
|
var networkLogCleanup = new Map;
|
|
31570
32207
|
var consoleCaptureCleanup = new Map;
|
|
31571
32208
|
var harCaptures = new Map;
|
|
@@ -31612,7 +32249,7 @@ var server = new McpServer({
|
|
|
31612
32249
|
name: "@hasna/browser",
|
|
31613
32250
|
version: "0.0.1"
|
|
31614
32251
|
});
|
|
31615
|
-
server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected.", {
|
|
32252
|
+
server.tool("browser_session_create", "Create a new browser session. If agent_id is set and already has an active session, returns the existing one (use force_new to override). If session_id is omitted on other tools, the single active session is auto-selected. Use cdp_url to attach to an already-running Chrome instance.", {
|
|
31616
32253
|
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
31617
32254
|
use_case: exports_external.string().optional(),
|
|
31618
32255
|
project_id: exports_external.string().optional(),
|
|
@@ -31623,9 +32260,11 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
|
|
|
31623
32260
|
viewport_height: exports_external.number().optional().default(720),
|
|
31624
32261
|
stealth: exports_external.boolean().optional().default(false),
|
|
31625
32262
|
auto_gallery: exports_external.boolean().optional().default(false),
|
|
32263
|
+
storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
|
|
31626
32264
|
force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
|
|
31627
|
-
tags: exports_external.array(exports_external.string()).optional()
|
|
31628
|
-
|
|
32265
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
32266
|
+
cdp_url: exports_external.string().optional().describe("Connect to existing Chrome via CDP (e.g. http://localhost:9222). Start Chrome with --remote-debugging-port=9222")
|
|
32267
|
+
}, 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 }) => {
|
|
31629
32268
|
try {
|
|
31630
32269
|
if (agent_id && !force_new) {
|
|
31631
32270
|
const existing = getActiveSessionForAgent2(agent_id);
|
|
@@ -31641,7 +32280,9 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
|
|
|
31641
32280
|
headless,
|
|
31642
32281
|
viewport: { width: viewport_width, height: viewport_height },
|
|
31643
32282
|
stealth,
|
|
31644
|
-
autoGallery: auto_gallery
|
|
32283
|
+
autoGallery: auto_gallery,
|
|
32284
|
+
storageState: storage_state,
|
|
32285
|
+
cdpUrl: cdp_url
|
|
31645
32286
|
});
|
|
31646
32287
|
if (tags?.length) {
|
|
31647
32288
|
const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -31801,7 +32442,7 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
|
|
|
31801
32442
|
return err(e);
|
|
31802
32443
|
}
|
|
31803
32444
|
});
|
|
31804
|
-
server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
|
|
32445
|
+
server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability. Self-healing auto-tries fallback selectors if element not found.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional(), self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found") }, async ({ session_id, selector, ref, button, timeout, self_heal }) => {
|
|
31805
32446
|
try {
|
|
31806
32447
|
const sid = resolveSessionId(session_id);
|
|
31807
32448
|
const page = getSessionPage(sid);
|
|
@@ -31812,14 +32453,17 @@ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS sel
|
|
|
31812
32453
|
}
|
|
31813
32454
|
if (!selector)
|
|
31814
32455
|
return err(new Error("Either ref or selector is required"));
|
|
31815
|
-
await click(page, selector, { button, timeout });
|
|
31816
|
-
logEvent(sid, "click", { selector, method: "selector" });
|
|
32456
|
+
const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
|
|
32457
|
+
logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
|
|
32458
|
+
if (healInfo.healed) {
|
|
32459
|
+
return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
32460
|
+
}
|
|
31817
32461
|
return json({ clicked: selector, method: "selector" });
|
|
31818
32462
|
} catch (e) {
|
|
31819
32463
|
return errWithScreenshot(e, session_id);
|
|
31820
32464
|
}
|
|
31821
32465
|
});
|
|
31822
|
-
server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
|
|
32466
|
+
server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref. Self-healing auto-tries fallback selectors if element not found.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional(), self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found") }, async ({ session_id, selector, ref, text, clear, delay, self_heal }) => {
|
|
31823
32467
|
try {
|
|
31824
32468
|
const sid = resolveSessionId(session_id);
|
|
31825
32469
|
const page = getSessionPage(sid);
|
|
@@ -31830,8 +32474,11 @@ server.tool("browser_type", "Type text into an element by ref or selector. Prefe
|
|
|
31830
32474
|
}
|
|
31831
32475
|
if (!selector)
|
|
31832
32476
|
return err(new Error("Either ref or selector is required"));
|
|
31833
|
-
await type(page, selector, text, { clear, delay });
|
|
31834
|
-
logEvent(sid, "type", { selector, text: text.slice(0, 100) });
|
|
32477
|
+
const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
|
|
32478
|
+
logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
|
|
32479
|
+
if (healInfo.healed) {
|
|
32480
|
+
return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
32481
|
+
}
|
|
31835
32482
|
return json({ typed: text, selector, method: "selector" });
|
|
31836
32483
|
} catch (e) {
|
|
31837
32484
|
return errWithScreenshot(e, session_id);
|
|
@@ -31925,20 +32572,32 @@ server.tool("browser_wait", "Wait for a selector to appear", { session_id: expor
|
|
|
31925
32572
|
return err(e);
|
|
31926
32573
|
}
|
|
31927
32574
|
});
|
|
31928
|
-
server.tool("browser_get_text", "Get text content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
|
|
32575
|
+
server.tool("browser_get_text", "Get text content from the page or a selector. Sanitizes prompt injection by default.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from text (default: true)") }, async ({ session_id, selector, sanitize }) => {
|
|
31929
32576
|
try {
|
|
31930
32577
|
const sid = resolveSessionId(session_id);
|
|
31931
32578
|
const page = getSessionPage(sid);
|
|
31932
|
-
|
|
32579
|
+
const text = await getText(page, selector);
|
|
32580
|
+
if (sanitize) {
|
|
32581
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
32582
|
+
const sanitized = sanitizeText2(text);
|
|
32583
|
+
return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
32584
|
+
}
|
|
32585
|
+
return json({ text });
|
|
31933
32586
|
} catch (e) {
|
|
31934
32587
|
return err(e);
|
|
31935
32588
|
}
|
|
31936
32589
|
});
|
|
31937
|
-
server.tool("browser_get_html", "Get HTML content from the page or a selector", { session_id: exports_external.string().optional(), selector: exports_external.string().optional() }, async ({ session_id, selector }) => {
|
|
32590
|
+
server.tool("browser_get_html", "Get HTML content from the page or a selector. Sanitizes prompt injection by default.", { session_id: exports_external.string().optional(), selector: exports_external.string().optional(), sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns and hidden elements from HTML (default: true)") }, async ({ session_id, selector, sanitize }) => {
|
|
31938
32591
|
try {
|
|
31939
32592
|
const sid = resolveSessionId(session_id);
|
|
31940
32593
|
const page = getSessionPage(sid);
|
|
31941
|
-
|
|
32594
|
+
const html = await getHTML(page, selector);
|
|
32595
|
+
if (sanitize) {
|
|
32596
|
+
const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
32597
|
+
const sanitized = sanitizeHTML2(html);
|
|
32598
|
+
return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
32599
|
+
}
|
|
32600
|
+
return json({ html });
|
|
31942
32601
|
} catch (e) {
|
|
31943
32602
|
return err(e);
|
|
31944
32603
|
}
|
|
@@ -31953,16 +32612,32 @@ server.tool("browser_get_links", "Get all links from the current page", { sessio
|
|
|
31953
32612
|
return err(e);
|
|
31954
32613
|
}
|
|
31955
32614
|
});
|
|
31956
|
-
server.tool("browser_extract", "Extract content from the page in a specified format", {
|
|
32615
|
+
server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
|
|
31957
32616
|
session_id: exports_external.string().optional(),
|
|
31958
32617
|
format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
|
|
31959
32618
|
selector: exports_external.string().optional(),
|
|
31960
|
-
schema: exports_external.record(exports_external.string()).optional()
|
|
31961
|
-
|
|
32619
|
+
schema: exports_external.record(exports_external.string()).optional(),
|
|
32620
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
|
|
32621
|
+
}, async ({ session_id, format, selector, schema, sanitize }) => {
|
|
31962
32622
|
try {
|
|
31963
32623
|
const sid = resolveSessionId(session_id);
|
|
31964
32624
|
const page = getSessionPage(sid);
|
|
31965
32625
|
const result = await extract(page, { format, selector, schema });
|
|
32626
|
+
if (sanitize) {
|
|
32627
|
+
const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
32628
|
+
if (result.text) {
|
|
32629
|
+
const s = sanitizeText2(result.text);
|
|
32630
|
+
result.text = s.text;
|
|
32631
|
+
result.stripped = s.stripped;
|
|
32632
|
+
result.warnings = s.warnings;
|
|
32633
|
+
}
|
|
32634
|
+
if (result.html) {
|
|
32635
|
+
const s = sanitizeHTML2(result.html);
|
|
32636
|
+
result.html = s.text;
|
|
32637
|
+
result.stripped = s.stripped;
|
|
32638
|
+
result.warnings = s.warnings;
|
|
32639
|
+
}
|
|
32640
|
+
}
|
|
31966
32641
|
return json(result);
|
|
31967
32642
|
} catch (e) {
|
|
31968
32643
|
return err(e);
|
|
@@ -31979,17 +32654,27 @@ server.tool("browser_find", "Find elements matching a selector and return their
|
|
|
31979
32654
|
return err(e);
|
|
31980
32655
|
}
|
|
31981
32656
|
});
|
|
31982
|
-
server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@e0, @e1...). Use compact=true (default) for token-efficient output. Use refs in browser_click, browser_type, etc.", {
|
|
32657
|
+
server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@e0, @e1...). Use compact=true (default) for token-efficient output. Use refs in browser_click, browser_type, etc. Sanitizes prompt injection by default.", {
|
|
31983
32658
|
session_id: exports_external.string().optional(),
|
|
31984
32659
|
compact: exports_external.boolean().optional().default(true),
|
|
31985
32660
|
max_refs: exports_external.number().optional().default(50),
|
|
31986
|
-
full_tree: exports_external.boolean().optional().default(false)
|
|
31987
|
-
|
|
32661
|
+
full_tree: exports_external.boolean().optional().default(false),
|
|
32662
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
|
|
32663
|
+
}, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
|
|
31988
32664
|
try {
|
|
31989
32665
|
const sid = resolveSessionId(session_id);
|
|
31990
32666
|
const page = getSessionPage(sid);
|
|
31991
32667
|
const result = await takeSnapshot(page, sid);
|
|
31992
32668
|
setLastSnapshot(sid, result);
|
|
32669
|
+
let injection_warnings;
|
|
32670
|
+
if (sanitize) {
|
|
32671
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
32672
|
+
const sanitized = sanitizeText2(result.tree);
|
|
32673
|
+
if (sanitized.stripped > 0) {
|
|
32674
|
+
injection_warnings = sanitized.warnings;
|
|
32675
|
+
result.tree = sanitized.text;
|
|
32676
|
+
}
|
|
32677
|
+
}
|
|
31993
32678
|
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
31994
32679
|
const limitedRefs = Object.fromEntries(refEntries);
|
|
31995
32680
|
const truncated = Object.keys(result.refs).length > max_refs;
|
|
@@ -32001,27 +32686,29 @@ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@
|
|
|
32001
32686
|
interactive_count: result.interactive_count,
|
|
32002
32687
|
shown_count: refEntries.length,
|
|
32003
32688
|
truncated,
|
|
32004
|
-
refs: limitedRefs
|
|
32689
|
+
refs: limitedRefs,
|
|
32690
|
+
...injection_warnings ? { injection_warnings } : {}
|
|
32005
32691
|
});
|
|
32006
32692
|
}
|
|
32007
32693
|
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
32008
32694
|
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
32009
|
-
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
32695
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
|
|
32010
32696
|
} catch (e) {
|
|
32011
32697
|
return err(e);
|
|
32012
32698
|
}
|
|
32013
32699
|
});
|
|
32014
|
-
server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements
|
|
32700
|
+
server.tool("browser_screenshot", "Take a screenshot. Use selector to capture a specific element/section instead of the full page. Use detail='high' for AI-readable full image, 'low' for fast thumbnail. Use annotate=true to overlay numbered labels on interactive elements.", {
|
|
32015
32701
|
session_id: exports_external.string().optional(),
|
|
32016
|
-
selector: exports_external.string().optional(),
|
|
32702
|
+
selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
|
|
32017
32703
|
full_page: exports_external.boolean().optional().default(false),
|
|
32018
32704
|
format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
|
|
32019
32705
|
quality: exports_external.number().optional().default(60),
|
|
32020
32706
|
max_width: exports_external.number().optional().default(800),
|
|
32021
32707
|
compress: exports_external.boolean().optional().default(true),
|
|
32022
32708
|
thumbnail: exports_external.boolean().optional().default(true),
|
|
32023
|
-
annotate: exports_external.boolean().optional().default(false)
|
|
32024
|
-
|
|
32709
|
+
annotate: exports_external.boolean().optional().default(false),
|
|
32710
|
+
detail: exports_external.enum(["low", "high"]).optional().default("low").describe("'low' = thumbnail only (fast, saves tokens). 'high' = full readable image in base64 (larger but AI can read text).")
|
|
32711
|
+
}, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
|
|
32025
32712
|
try {
|
|
32026
32713
|
const sid = resolveSessionId(session_id);
|
|
32027
32714
|
const page = getSessionPage(sid);
|
|
@@ -32038,7 +32725,9 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
|
|
|
32038
32725
|
annotation_count: annotated.annotations.length
|
|
32039
32726
|
});
|
|
32040
32727
|
}
|
|
32041
|
-
const
|
|
32728
|
+
const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
|
|
32729
|
+
const effectiveQuality = detail === "high" ? 75 : quality;
|
|
32730
|
+
const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
|
|
32042
32731
|
result.url = page.url();
|
|
32043
32732
|
try {
|
|
32044
32733
|
const buf = Buffer.from(result.base64, "base64");
|
|
@@ -32047,12 +32736,12 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
|
|
|
32047
32736
|
result.download_id = dl.id;
|
|
32048
32737
|
} catch {}
|
|
32049
32738
|
result.estimated_tokens = Math.ceil(result.base64.length / 4);
|
|
32050
|
-
if (result.base64.length >
|
|
32739
|
+
if (detail !== "high" && result.base64.length > 40000) {
|
|
32051
32740
|
result.base64_truncated = true;
|
|
32052
32741
|
result.full_image_path = result.path;
|
|
32053
32742
|
result.base64 = result.thumbnail_base64 ?? "";
|
|
32054
32743
|
}
|
|
32055
|
-
logEvent(sid, "screenshot", { path: result.path });
|
|
32744
|
+
logEvent(sid, "screenshot", { path: result.path, detail, selector });
|
|
32056
32745
|
return json(result);
|
|
32057
32746
|
} catch (e) {
|
|
32058
32747
|
return err(e);
|
|
@@ -32458,6 +33147,94 @@ server.tool("browser_session_untag", "Remove a tag from a session", { session_id
|
|
|
32458
33147
|
return err(e);
|
|
32459
33148
|
}
|
|
32460
33149
|
});
|
|
33150
|
+
server.tool("browser_session_save_state", "Save current session's auth state (cookies, localStorage) for reuse. Use after login to avoid re-authenticating.", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Name for this state (e.g. 'github', 'gmail')") }, async ({ session_id, name }) => {
|
|
33151
|
+
try {
|
|
33152
|
+
const sid = resolveSessionId(session_id);
|
|
33153
|
+
const page = getSessionPage(sid);
|
|
33154
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33155
|
+
const path = await saveStateFromPage2(page, name);
|
|
33156
|
+
return json({ saved: true, name, path });
|
|
33157
|
+
} catch (e) {
|
|
33158
|
+
return err(e);
|
|
33159
|
+
}
|
|
33160
|
+
});
|
|
33161
|
+
server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
|
|
33162
|
+
try {
|
|
33163
|
+
const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33164
|
+
const states = listStates2();
|
|
33165
|
+
return json({ states, count: states.length });
|
|
33166
|
+
} catch (e) {
|
|
33167
|
+
return err(e);
|
|
33168
|
+
}
|
|
33169
|
+
});
|
|
33170
|
+
server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
|
|
33171
|
+
try {
|
|
33172
|
+
const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33173
|
+
return json({ deleted: deleteState2(name), name });
|
|
33174
|
+
} catch (e) {
|
|
33175
|
+
return err(e);
|
|
33176
|
+
}
|
|
33177
|
+
});
|
|
33178
|
+
server.tool("browser_auth_record", "Start recording a login flow. Navigate to the login page, perform the login, then call browser_auth_stop to save.", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Name for this auth flow (e.g. 'github', 'gmail')"), start_url: exports_external.string().optional().describe("Login page URL") }, async ({ session_id, name, start_url }) => {
|
|
33179
|
+
try {
|
|
33180
|
+
const sid = resolveSessionId(session_id);
|
|
33181
|
+
const page = getSessionPage(sid);
|
|
33182
|
+
if (start_url)
|
|
33183
|
+
await navigate(page, start_url);
|
|
33184
|
+
const recording = startRecording(sid, `auth-${name}`, page.url());
|
|
33185
|
+
return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
|
|
33186
|
+
} catch (e) {
|
|
33187
|
+
return err(e);
|
|
33188
|
+
}
|
|
33189
|
+
});
|
|
33190
|
+
server.tool("browser_auth_stop", "Stop recording a login flow and save as a reusable auth flow with storage state.", { session_id: exports_external.string().optional(), name: exports_external.string(), recording_id: exports_external.string() }, async ({ session_id, name, recording_id }) => {
|
|
33191
|
+
try {
|
|
33192
|
+
const sid = resolveSessionId(session_id);
|
|
33193
|
+
const page = getSessionPage(sid);
|
|
33194
|
+
const recording = stopRecording(recording_id);
|
|
33195
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33196
|
+
const statePath2 = await saveStateFromPage2(page, name);
|
|
33197
|
+
let domain = "";
|
|
33198
|
+
try {
|
|
33199
|
+
domain = new URL(page.url()).hostname;
|
|
33200
|
+
} catch {}
|
|
33201
|
+
const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33202
|
+
const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
|
|
33203
|
+
return json({ flow, recording_steps: recording.steps.length });
|
|
33204
|
+
} catch (e) {
|
|
33205
|
+
return err(e);
|
|
33206
|
+
}
|
|
33207
|
+
});
|
|
33208
|
+
server.tool("browser_auth_replay", "Manually replay a saved auth flow for a domain", { session_id: exports_external.string().optional(), name: exports_external.string().describe("Auth flow name to replay") }, async ({ session_id, name }) => {
|
|
33209
|
+
try {
|
|
33210
|
+
const sid = resolveSessionId(session_id);
|
|
33211
|
+
const page = getSessionPage(sid);
|
|
33212
|
+
const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33213
|
+
const flow = getAuthFlowByName2(name);
|
|
33214
|
+
if (!flow)
|
|
33215
|
+
return err(new Error(`Auth flow '${name}' not found`));
|
|
33216
|
+
const result = await tryReplayAuth2(page, flow.domain);
|
|
33217
|
+
return json(result);
|
|
33218
|
+
} catch (e) {
|
|
33219
|
+
return err(e);
|
|
33220
|
+
}
|
|
33221
|
+
});
|
|
33222
|
+
server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
|
|
33223
|
+
try {
|
|
33224
|
+
const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33225
|
+
return json({ flows: listAuthFlows2() });
|
|
33226
|
+
} catch (e) {
|
|
33227
|
+
return err(e);
|
|
33228
|
+
}
|
|
33229
|
+
});
|
|
33230
|
+
server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
|
|
33231
|
+
try {
|
|
33232
|
+
const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33233
|
+
return json({ deleted: deleteAuthFlow2(name) });
|
|
33234
|
+
} catch (e) {
|
|
33235
|
+
return err(e);
|
|
33236
|
+
}
|
|
33237
|
+
});
|
|
32461
33238
|
server.tool("browser_click_text", "Click an element by its visible text content", { session_id: exports_external.string().optional(), text: exports_external.string(), exact: exports_external.boolean().optional().default(false), timeout: exports_external.number().optional() }, async ({ session_id, text, exact, timeout }) => {
|
|
32462
33239
|
try {
|
|
32463
33240
|
const sid = resolveSessionId(session_id);
|
|
@@ -32468,20 +33245,45 @@ server.tool("browser_click_text", "Click an element by its visible text content"
|
|
|
32468
33245
|
return err(e);
|
|
32469
33246
|
}
|
|
32470
33247
|
});
|
|
32471
|
-
server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
|
|
33248
|
+
server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects. Self-healing auto-tries fallback selectors per field.", {
|
|
32472
33249
|
session_id: exports_external.string().optional(),
|
|
32473
33250
|
fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
|
|
32474
|
-
submit_selector: exports_external.string().optional()
|
|
32475
|
-
|
|
33251
|
+
submit_selector: exports_external.string().optional(),
|
|
33252
|
+
self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
|
|
33253
|
+
}, async ({ session_id, fields, submit_selector, self_heal }) => {
|
|
32476
33254
|
try {
|
|
32477
33255
|
const sid = resolveSessionId(session_id);
|
|
32478
33256
|
const page = getSessionPage(sid);
|
|
32479
|
-
const result = await fillForm(page, fields, submit_selector);
|
|
33257
|
+
const result = await fillForm(page, fields, submit_selector, self_heal);
|
|
32480
33258
|
return json(result);
|
|
32481
33259
|
} catch (e) {
|
|
32482
33260
|
return errWithScreenshot(e, session_id);
|
|
32483
33261
|
}
|
|
32484
33262
|
});
|
|
33263
|
+
server.tool("browser_find_visual", "Find an element using AI vision when selectors and a11y refs fail. Useful for canvas, images, custom widgets. Takes a screenshot and asks a vision model to locate the element.", {
|
|
33264
|
+
session_id: exports_external.string().optional(),
|
|
33265
|
+
description: exports_external.string().describe("Natural language description of the element to find (e.g. 'the blue Submit button', 'the search icon in the top right')"),
|
|
33266
|
+
click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
|
|
33267
|
+
model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
|
|
33268
|
+
}, async ({ session_id, description, click: doClick, model }) => {
|
|
33269
|
+
try {
|
|
33270
|
+
const sid = resolveSessionId(session_id);
|
|
33271
|
+
const page = getSessionPage(sid);
|
|
33272
|
+
if (doClick) {
|
|
33273
|
+
const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
33274
|
+
const result = await clickByVision2(page, description, { model });
|
|
33275
|
+
logEvent(sid, "vision_click", { query: description, ...result });
|
|
33276
|
+
return json(result);
|
|
33277
|
+
} else {
|
|
33278
|
+
const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
33279
|
+
const result = await findElementByVision2(page, description, { model });
|
|
33280
|
+
logEvent(sid, "vision_find", { query: description, ...result });
|
|
33281
|
+
return json(result);
|
|
33282
|
+
}
|
|
33283
|
+
} catch (e) {
|
|
33284
|
+
return err(e);
|
|
33285
|
+
}
|
|
33286
|
+
});
|
|
32485
33287
|
server.tool("browser_wait_for_text", "Wait until specific text appears on the page", { session_id: exports_external.string().optional(), text: exports_external.string(), timeout: exports_external.number().optional().default(1e4), exact: exports_external.boolean().optional().default(false) }, async ({ session_id, text, timeout, exact }) => {
|
|
32486
33288
|
try {
|
|
32487
33289
|
const sid = resolveSessionId(session_id);
|
|
@@ -32902,6 +33704,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32902
33704
|
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
32903
33705
|
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
32904
33706
|
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
33707
|
+
{ tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
|
|
32905
33708
|
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
32906
33709
|
],
|
|
32907
33710
|
Extraction: [
|
|
@@ -32930,7 +33733,10 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32930
33733
|
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
32931
33734
|
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
32932
33735
|
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
32933
|
-
{ tool: "browser_profile_delete", description: "Delete a saved profile" }
|
|
33736
|
+
{ tool: "browser_profile_delete", description: "Delete a saved profile" },
|
|
33737
|
+
{ tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
|
|
33738
|
+
{ tool: "browser_session_list_states", description: "List saved storage states" },
|
|
33739
|
+
{ tool: "browser_session_delete_state", description: "Delete a saved storage state" }
|
|
32934
33740
|
],
|
|
32935
33741
|
Network: [
|
|
32936
33742
|
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
@@ -32954,6 +33760,13 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32954
33760
|
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
32955
33761
|
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
32956
33762
|
],
|
|
33763
|
+
Auth: [
|
|
33764
|
+
{ tool: "browser_auth_record", description: "Start recording a login flow" },
|
|
33765
|
+
{ tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
|
|
33766
|
+
{ tool: "browser_auth_replay", description: "Replay a saved auth flow" },
|
|
33767
|
+
{ tool: "browser_auth_list", description: "List all saved auth flows" },
|
|
33768
|
+
{ tool: "browser_auth_delete", description: "Delete a saved auth flow" }
|
|
33769
|
+
],
|
|
32957
33770
|
Crawl: [
|
|
32958
33771
|
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
32959
33772
|
],
|
|
@@ -33010,7 +33823,8 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
33010
33823
|
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
33011
33824
|
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
33012
33825
|
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
33013
|
-
{ tool: "browser_watch_stop", description: "Stop DOM watcher" }
|
|
33826
|
+
{ tool: "browser_watch_stop", description: "Stop DOM watcher" },
|
|
33827
|
+
{ tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
|
|
33014
33828
|
]
|
|
33015
33829
|
};
|
|
33016
33830
|
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
@@ -33293,6 +34107,82 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
|
|
|
33293
34107
|
return err(e);
|
|
33294
34108
|
}
|
|
33295
34109
|
});
|
|
34110
|
+
server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
|
|
34111
|
+
actions: exports_external.array(exports_external.object({
|
|
34112
|
+
session_id: exports_external.string().describe("Target session ID"),
|
|
34113
|
+
tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
|
|
34114
|
+
args: exports_external.record(exports_external.unknown()).optional().default({})
|
|
34115
|
+
})),
|
|
34116
|
+
timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
|
|
34117
|
+
}, async ({ actions, timeout }) => {
|
|
34118
|
+
try {
|
|
34119
|
+
const t0 = Date.now();
|
|
34120
|
+
const promises = actions.map(async (action, index) => {
|
|
34121
|
+
try {
|
|
34122
|
+
const sid = action.session_id;
|
|
34123
|
+
const page = getSessionPage(sid);
|
|
34124
|
+
const args = action.args;
|
|
34125
|
+
const toolName = action.tool.replace(/^browser_/, "");
|
|
34126
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
|
|
34127
|
+
const actionPromise = (async () => {
|
|
34128
|
+
switch (toolName) {
|
|
34129
|
+
case "navigate": {
|
|
34130
|
+
await navigate(page, args.url);
|
|
34131
|
+
const title = await page.title();
|
|
34132
|
+
return { url: page.url(), title };
|
|
34133
|
+
}
|
|
34134
|
+
case "screenshot": {
|
|
34135
|
+
const result2 = await takeScreenshot(page, {
|
|
34136
|
+
maxWidth: args.max_width ?? 800,
|
|
34137
|
+
quality: args.quality ?? 60
|
|
34138
|
+
});
|
|
34139
|
+
return { path: result2.path, size_bytes: result2.size_bytes };
|
|
34140
|
+
}
|
|
34141
|
+
case "click": {
|
|
34142
|
+
if (args.selector)
|
|
34143
|
+
await click(page, args.selector);
|
|
34144
|
+
return { clicked: args.selector };
|
|
34145
|
+
}
|
|
34146
|
+
case "type": {
|
|
34147
|
+
if (args.selector && args.text)
|
|
34148
|
+
await type(page, args.selector, args.text);
|
|
34149
|
+
return { typed: args.text };
|
|
34150
|
+
}
|
|
34151
|
+
case "get_text": {
|
|
34152
|
+
const text = await getText(page);
|
|
34153
|
+
return { text: text.slice(0, 1000), length: text.length };
|
|
34154
|
+
}
|
|
34155
|
+
case "get_links": {
|
|
34156
|
+
const links = await getLinks(page);
|
|
34157
|
+
return { links, count: links.length };
|
|
34158
|
+
}
|
|
34159
|
+
case "snapshot": {
|
|
34160
|
+
const snap = await takeSnapshot(page, sid);
|
|
34161
|
+
return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
|
|
34162
|
+
}
|
|
34163
|
+
case "evaluate": {
|
|
34164
|
+
const result2 = await page.evaluate(args.expression);
|
|
34165
|
+
return { result: result2 };
|
|
34166
|
+
}
|
|
34167
|
+
default:
|
|
34168
|
+
return { error: `Unknown tool: ${action.tool}` };
|
|
34169
|
+
}
|
|
34170
|
+
})();
|
|
34171
|
+
const result = await Promise.race([actionPromise, timeoutPromise]);
|
|
34172
|
+
return { index, session_id: sid, tool: action.tool, success: true, result };
|
|
34173
|
+
} catch (e) {
|
|
34174
|
+
return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
|
|
34175
|
+
}
|
|
34176
|
+
});
|
|
34177
|
+
const results = await Promise.all(promises);
|
|
34178
|
+
const duration_ms = Date.now() - t0;
|
|
34179
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
34180
|
+
const failed = results.filter((r) => !r.success).length;
|
|
34181
|
+
return json({ results, duration_ms, succeeded, failed, total: actions.length });
|
|
34182
|
+
} catch (e) {
|
|
34183
|
+
return err(e);
|
|
34184
|
+
}
|
|
34185
|
+
});
|
|
33296
34186
|
server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
|
|
33297
34187
|
try {
|
|
33298
34188
|
return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
|