@hasna/browser 0.0.9 → 0.1.1
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 +2090 -364
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/index.js +545 -214
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/api-detector.d.ts +17 -0
- package/dist/lib/api-detector.d.ts.map +1 -0
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/datasets.d.ts +33 -0
- package/dist/lib/datasets.d.ts.map +1 -0
- package/dist/lib/deep-performance.d.ts +49 -0
- package/dist/lib/deep-performance.d.ts.map +1 -0
- package/dist/lib/env-detector.d.ts +12 -0
- package/dist/lib/env-detector.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/structured-extract.d.ts +26 -0
- package/dist/lib/structured-extract.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/lib/workflows.d.ts +46 -0
- package/dist/lib/workflows.d.ts.map +1 -0
- package/dist/mcp/index.js +2196 -485
- package/dist/server/index.js +422 -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,71 @@ 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
|
+
`
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
version: 7,
|
|
330
|
+
sql: `
|
|
331
|
+
CREATE TABLE IF NOT EXISTS workflows (
|
|
332
|
+
id TEXT PRIMARY KEY,
|
|
333
|
+
name TEXT NOT NULL UNIQUE,
|
|
334
|
+
description TEXT,
|
|
335
|
+
steps TEXT NOT NULL DEFAULT '[]',
|
|
336
|
+
start_url TEXT,
|
|
337
|
+
last_run TEXT,
|
|
338
|
+
last_heal TEXT,
|
|
339
|
+
heal_count INTEGER DEFAULT 0,
|
|
340
|
+
run_count INTEGER DEFAULT 0,
|
|
341
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
342
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
343
|
+
);
|
|
344
|
+
`
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
version: 8,
|
|
348
|
+
sql: `
|
|
349
|
+
CREATE TABLE IF NOT EXISTS datasets (
|
|
350
|
+
id TEXT PRIMARY KEY,
|
|
351
|
+
name TEXT NOT NULL UNIQUE,
|
|
352
|
+
source_url TEXT,
|
|
353
|
+
source_type TEXT NOT NULL DEFAULT 'page',
|
|
354
|
+
data TEXT NOT NULL DEFAULT '[]',
|
|
355
|
+
schema TEXT,
|
|
356
|
+
row_count INTEGER DEFAULT 0,
|
|
357
|
+
last_refresh TEXT,
|
|
358
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
359
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
CREATE TABLE IF NOT EXISTS api_endpoints (
|
|
363
|
+
id TEXT PRIMARY KEY,
|
|
364
|
+
session_id TEXT,
|
|
365
|
+
url TEXT NOT NULL,
|
|
366
|
+
method TEXT DEFAULT 'GET',
|
|
367
|
+
response_schema TEXT,
|
|
368
|
+
sample_response TEXT,
|
|
369
|
+
status_code INTEGER,
|
|
370
|
+
content_type TEXT,
|
|
371
|
+
discovered_at TEXT DEFAULT (datetime('now'))
|
|
372
|
+
);
|
|
373
|
+
CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
|
|
374
|
+
`
|
|
310
375
|
}
|
|
311
376
|
];
|
|
312
377
|
for (const m of migrations) {
|
|
@@ -1267,14 +1332,14 @@ function enableConsoleCapture(page, sessionId) {
|
|
|
1267
1332
|
warning: "warn"
|
|
1268
1333
|
};
|
|
1269
1334
|
const level = levelMap[msg.type()] ?? "log";
|
|
1270
|
-
const
|
|
1335
|
+
const location2 = msg.location();
|
|
1271
1336
|
try {
|
|
1272
1337
|
logConsoleMessage({
|
|
1273
1338
|
session_id: sessionId,
|
|
1274
1339
|
level,
|
|
1275
1340
|
message: msg.text(),
|
|
1276
|
-
source:
|
|
1277
|
-
line_number:
|
|
1341
|
+
source: location2.url || undefined,
|
|
1342
|
+
line_number: location2.lineNumber || undefined
|
|
1278
1343
|
});
|
|
1279
1344
|
} catch {}
|
|
1280
1345
|
};
|
|
@@ -1418,6 +1483,188 @@ var init_dialogs = __esm(() => {
|
|
|
1418
1483
|
pendingDialogs = new Map;
|
|
1419
1484
|
});
|
|
1420
1485
|
|
|
1486
|
+
// src/engines/cdp.ts
|
|
1487
|
+
var exports_cdp = {};
|
|
1488
|
+
__export(exports_cdp, {
|
|
1489
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
1490
|
+
CDPClient: () => CDPClient
|
|
1491
|
+
});
|
|
1492
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
1493
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
1494
|
+
try {
|
|
1495
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
1496
|
+
} catch (err) {
|
|
1497
|
+
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);
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
class CDPClient {
|
|
1502
|
+
session;
|
|
1503
|
+
networkEnabled = false;
|
|
1504
|
+
performanceEnabled = false;
|
|
1505
|
+
constructor(session) {
|
|
1506
|
+
this.session = session;
|
|
1507
|
+
}
|
|
1508
|
+
static async fromPage(page) {
|
|
1509
|
+
try {
|
|
1510
|
+
const session = await page.context().newCDPSession(page);
|
|
1511
|
+
return new CDPClient(session);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async send(method, params) {
|
|
1517
|
+
try {
|
|
1518
|
+
return await this.session.send(method, params);
|
|
1519
|
+
} catch (err) {
|
|
1520
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
on(event, handler) {
|
|
1524
|
+
this.session.on(event, handler);
|
|
1525
|
+
}
|
|
1526
|
+
off(event, handler) {
|
|
1527
|
+
this.session.off(event, handler);
|
|
1528
|
+
}
|
|
1529
|
+
async enableNetwork() {
|
|
1530
|
+
if (!this.networkEnabled) {
|
|
1531
|
+
await this.send("Network.enable");
|
|
1532
|
+
this.networkEnabled = true;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async enablePerformance() {
|
|
1536
|
+
if (!this.performanceEnabled) {
|
|
1537
|
+
await this.send("Performance.enable");
|
|
1538
|
+
this.performanceEnabled = true;
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
async getPerformanceMetrics() {
|
|
1542
|
+
await this.enablePerformance();
|
|
1543
|
+
const result = await this.send("Performance.getMetrics");
|
|
1544
|
+
const m = {};
|
|
1545
|
+
for (const metric of result.metrics) {
|
|
1546
|
+
m[metric.name] = metric.value;
|
|
1547
|
+
}
|
|
1548
|
+
return {
|
|
1549
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
1550
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
1551
|
+
dom_interactive: m["DOMInteractive"],
|
|
1552
|
+
dom_complete: m["DOMComplete"],
|
|
1553
|
+
load_event: m["LoadEventEnd"]
|
|
1554
|
+
};
|
|
1555
|
+
}
|
|
1556
|
+
async startJSCoverage() {
|
|
1557
|
+
await this.send("Profiler.enable");
|
|
1558
|
+
await this.send("Debugger.enable");
|
|
1559
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
1560
|
+
callCount: false,
|
|
1561
|
+
detailed: true
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
async stopJSCoverage() {
|
|
1565
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
1566
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
1567
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
1568
|
+
url: r.url,
|
|
1569
|
+
text: "",
|
|
1570
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
1571
|
+
}));
|
|
1572
|
+
}
|
|
1573
|
+
async getCoverage() {
|
|
1574
|
+
await this.startJSCoverage();
|
|
1575
|
+
const js = await this.stopJSCoverage();
|
|
1576
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
1577
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
1578
|
+
}
|
|
1579
|
+
async captureHAREntries(page, handler) {
|
|
1580
|
+
await this.enableNetwork();
|
|
1581
|
+
const requestTimings = new Map;
|
|
1582
|
+
const onRequest = (params) => {
|
|
1583
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
1584
|
+
};
|
|
1585
|
+
const onResponse = (params) => {
|
|
1586
|
+
const start = requestTimings.get(params.requestId);
|
|
1587
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
1588
|
+
handler({
|
|
1589
|
+
method: "GET",
|
|
1590
|
+
url: params.response.url,
|
|
1591
|
+
status: params.response.status,
|
|
1592
|
+
duration
|
|
1593
|
+
});
|
|
1594
|
+
};
|
|
1595
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
1596
|
+
this.on("Network.responseReceived", onResponse);
|
|
1597
|
+
return () => {
|
|
1598
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
1599
|
+
this.off("Network.responseReceived", onResponse);
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
async detach() {
|
|
1603
|
+
try {
|
|
1604
|
+
await this.session.detach();
|
|
1605
|
+
} catch {}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
var init_cdp = __esm(() => {
|
|
1609
|
+
init_types();
|
|
1610
|
+
});
|
|
1611
|
+
|
|
1612
|
+
// src/lib/storage-state.ts
|
|
1613
|
+
var exports_storage_state = {};
|
|
1614
|
+
__export(exports_storage_state, {
|
|
1615
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
1616
|
+
saveState: () => saveState,
|
|
1617
|
+
loadStatePath: () => loadStatePath,
|
|
1618
|
+
listStates: () => listStates,
|
|
1619
|
+
deleteState: () => deleteState
|
|
1620
|
+
});
|
|
1621
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
1622
|
+
import { join as join3 } from "path";
|
|
1623
|
+
import { homedir as homedir3 } from "os";
|
|
1624
|
+
function ensureDir() {
|
|
1625
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
1626
|
+
}
|
|
1627
|
+
function statePath(name) {
|
|
1628
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
1629
|
+
}
|
|
1630
|
+
async function saveState(context, name) {
|
|
1631
|
+
ensureDir();
|
|
1632
|
+
const path = statePath(name);
|
|
1633
|
+
const state = await context.storageState({ path });
|
|
1634
|
+
return path;
|
|
1635
|
+
}
|
|
1636
|
+
async function saveStateFromPage(page, name) {
|
|
1637
|
+
return saveState(page.context(), name);
|
|
1638
|
+
}
|
|
1639
|
+
function loadStatePath(name) {
|
|
1640
|
+
const path = statePath(name);
|
|
1641
|
+
return existsSync(path) ? path : null;
|
|
1642
|
+
}
|
|
1643
|
+
function listStates() {
|
|
1644
|
+
ensureDir();
|
|
1645
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
1646
|
+
const path = join3(STATES_DIR, f);
|
|
1647
|
+
const stat = Bun.file(path);
|
|
1648
|
+
return {
|
|
1649
|
+
name: f.replace(".json", ""),
|
|
1650
|
+
path,
|
|
1651
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
1652
|
+
};
|
|
1653
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
1654
|
+
}
|
|
1655
|
+
function deleteState(name) {
|
|
1656
|
+
const path = statePath(name);
|
|
1657
|
+
if (existsSync(path)) {
|
|
1658
|
+
unlinkSync(path);
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
return false;
|
|
1662
|
+
}
|
|
1663
|
+
var STATES_DIR;
|
|
1664
|
+
var init_storage_state = __esm(() => {
|
|
1665
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
1666
|
+
});
|
|
1667
|
+
|
|
1421
1668
|
// src/lib/session.ts
|
|
1422
1669
|
var exports_session = {};
|
|
1423
1670
|
__export(exports_session, {
|
|
@@ -1447,6 +1694,37 @@ function createBunProxy(view) {
|
|
|
1447
1694
|
return view;
|
|
1448
1695
|
}
|
|
1449
1696
|
async function createSession2(opts = {}) {
|
|
1697
|
+
if (opts.cdpUrl) {
|
|
1698
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
1699
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
1700
|
+
const contexts = cdpBrowser.contexts();
|
|
1701
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
1702
|
+
const pages = context.pages();
|
|
1703
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
1704
|
+
const session2 = createSession({
|
|
1705
|
+
engine: "cdp",
|
|
1706
|
+
projectId: opts.projectId,
|
|
1707
|
+
agentId: opts.agentId,
|
|
1708
|
+
startUrl: page2.url(),
|
|
1709
|
+
name: opts.name ?? "attached"
|
|
1710
|
+
});
|
|
1711
|
+
const cleanups2 = [];
|
|
1712
|
+
if (opts.captureNetwork !== false) {
|
|
1713
|
+
try {
|
|
1714
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
1715
|
+
} catch {}
|
|
1716
|
+
}
|
|
1717
|
+
if (opts.captureConsole !== false) {
|
|
1718
|
+
try {
|
|
1719
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
1720
|
+
} catch {}
|
|
1721
|
+
}
|
|
1722
|
+
try {
|
|
1723
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
1724
|
+
} catch {}
|
|
1725
|
+
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 });
|
|
1726
|
+
return { session: session2, page: page2 };
|
|
1727
|
+
}
|
|
1450
1728
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
1451
1729
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
1452
1730
|
let browser = null;
|
|
@@ -1472,7 +1750,22 @@ async function createSession2(opts = {}) {
|
|
|
1472
1750
|
page = await context.newPage();
|
|
1473
1751
|
} else {
|
|
1474
1752
|
browser = await pool.acquire(opts.headless ?? true);
|
|
1475
|
-
|
|
1753
|
+
if (opts.storageState) {
|
|
1754
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
1755
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
1756
|
+
if (statePath2) {
|
|
1757
|
+
const context = await browser.newContext({
|
|
1758
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
1759
|
+
userAgent: opts.userAgent,
|
|
1760
|
+
storageState: statePath2
|
|
1761
|
+
});
|
|
1762
|
+
page = await context.newPage();
|
|
1763
|
+
} else {
|
|
1764
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
1765
|
+
}
|
|
1766
|
+
} else {
|
|
1767
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
1768
|
+
}
|
|
1476
1769
|
}
|
|
1477
1770
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
1478
1771
|
try {
|
|
@@ -1977,6 +2270,66 @@ var init_snapshot = __esm(() => {
|
|
|
1977
2270
|
];
|
|
1978
2271
|
});
|
|
1979
2272
|
|
|
2273
|
+
// src/lib/self-heal.ts
|
|
2274
|
+
async function healSelector(page, selector, sessionId) {
|
|
2275
|
+
const attempts = [];
|
|
2276
|
+
attempts.push(`selector: ${selector}`);
|
|
2277
|
+
try {
|
|
2278
|
+
const loc = page.locator(selector).first();
|
|
2279
|
+
if (await loc.count() > 0) {
|
|
2280
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
2281
|
+
}
|
|
2282
|
+
} catch {}
|
|
2283
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
2284
|
+
attempts.push(`text: "${selector}"`);
|
|
2285
|
+
try {
|
|
2286
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
2287
|
+
if (await loc.count() > 0) {
|
|
2288
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
2289
|
+
}
|
|
2290
|
+
} catch {}
|
|
2291
|
+
}
|
|
2292
|
+
const roleMap = {
|
|
2293
|
+
button: ["button", "submit", "reset"],
|
|
2294
|
+
link: ["a"],
|
|
2295
|
+
input: ["input", "textarea"],
|
|
2296
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
2297
|
+
};
|
|
2298
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
2299
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
2300
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
2301
|
+
try {
|
|
2302
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
2303
|
+
if (await loc.count() > 0) {
|
|
2304
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
2305
|
+
}
|
|
2306
|
+
} catch {}
|
|
2307
|
+
}
|
|
2308
|
+
if (selector.startsWith("#")) {
|
|
2309
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
2310
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
2311
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
2312
|
+
try {
|
|
2313
|
+
const loc = page.locator(partialSel).first();
|
|
2314
|
+
if (await loc.count() > 0) {
|
|
2315
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
2316
|
+
}
|
|
2317
|
+
} catch {}
|
|
2318
|
+
}
|
|
2319
|
+
if (selector.startsWith(".")) {
|
|
2320
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
2321
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
2322
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
2323
|
+
try {
|
|
2324
|
+
const loc = page.locator(partialSel).first();
|
|
2325
|
+
if (await loc.count() > 0) {
|
|
2326
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
2327
|
+
}
|
|
2328
|
+
} catch {}
|
|
2329
|
+
}
|
|
2330
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
2331
|
+
}
|
|
2332
|
+
|
|
1980
2333
|
// src/lib/actions.ts
|
|
1981
2334
|
var exports_actions = {};
|
|
1982
2335
|
__export(exports_actions, {
|
|
@@ -2018,11 +2371,22 @@ async function click(page, selector, opts) {
|
|
|
2018
2371
|
delay: opts?.delay,
|
|
2019
2372
|
timeout: opts?.timeout ?? 1e4
|
|
2020
2373
|
});
|
|
2021
|
-
|
|
2022
|
-
|
|
2374
|
+
return {};
|
|
2375
|
+
} catch (originalError) {
|
|
2376
|
+
if (opts?.selfHeal !== false) {
|
|
2377
|
+
const result = await healSelector(page, selector);
|
|
2378
|
+
if (result.found && result.locator) {
|
|
2379
|
+
await result.locator.click({
|
|
2380
|
+
button: opts?.button ?? "left",
|
|
2381
|
+
timeout: opts?.timeout ?? 1e4
|
|
2382
|
+
});
|
|
2383
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
2023
2387
|
throw new ElementNotFoundError(selector);
|
|
2024
2388
|
}
|
|
2025
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
2389
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
2026
2390
|
}
|
|
2027
2391
|
}
|
|
2028
2392
|
async function type(page, selector, text, opts) {
|
|
@@ -2031,17 +2395,35 @@ async function type(page, selector, text, opts) {
|
|
|
2031
2395
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
2032
2396
|
}
|
|
2033
2397
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
2034
|
-
|
|
2035
|
-
|
|
2398
|
+
return {};
|
|
2399
|
+
} catch (originalError) {
|
|
2400
|
+
if (opts?.selfHeal !== false) {
|
|
2401
|
+
const result = await healSelector(page, selector);
|
|
2402
|
+
if (result.found && result.locator) {
|
|
2403
|
+
if (opts?.clear)
|
|
2404
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
2405
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
2406
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
2036
2410
|
throw new ElementNotFoundError(selector);
|
|
2037
2411
|
}
|
|
2038
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
2412
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
2039
2413
|
}
|
|
2040
2414
|
}
|
|
2041
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
2415
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
2042
2416
|
try {
|
|
2043
2417
|
await page.fill(selector, value, { timeout });
|
|
2044
|
-
|
|
2418
|
+
return {};
|
|
2419
|
+
} catch (originalError) {
|
|
2420
|
+
if (selfHeal) {
|
|
2421
|
+
const result = await healSelector(page, selector);
|
|
2422
|
+
if (result.found && result.locator) {
|
|
2423
|
+
await result.locator.fill(value, { timeout });
|
|
2424
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2045
2427
|
throw new ElementNotFoundError(selector);
|
|
2046
2428
|
}
|
|
2047
2429
|
}
|
|
@@ -2166,12 +2548,39 @@ async function clickText(page, text, opts) {
|
|
|
2166
2548
|
}
|
|
2167
2549
|
}, { retries: opts?.retries ?? 1 });
|
|
2168
2550
|
}
|
|
2169
|
-
async function fillForm(page, fields, submitSelector) {
|
|
2551
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
2170
2552
|
let filled = 0;
|
|
2171
2553
|
const errors2 = [];
|
|
2554
|
+
const healedFields = [];
|
|
2172
2555
|
for (const [selector, value] of Object.entries(fields)) {
|
|
2173
2556
|
try {
|
|
2174
|
-
|
|
2557
|
+
let el = await page.$(selector);
|
|
2558
|
+
if (!el && selfHeal) {
|
|
2559
|
+
const result = await healSelector(page, selector);
|
|
2560
|
+
if (result.found && result.locator) {
|
|
2561
|
+
const handle = await result.locator.elementHandle();
|
|
2562
|
+
if (handle) {
|
|
2563
|
+
el = handle;
|
|
2564
|
+
healedFields.push(selector);
|
|
2565
|
+
const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
|
|
2566
|
+
const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
|
|
2567
|
+
if (tagName2 === "select") {
|
|
2568
|
+
await result.locator.selectOption(String(value));
|
|
2569
|
+
} else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
|
|
2570
|
+
if (Boolean(value))
|
|
2571
|
+
await result.locator.check();
|
|
2572
|
+
else
|
|
2573
|
+
await result.locator.uncheck();
|
|
2574
|
+
} else {
|
|
2575
|
+
await result.locator.fill(String(value));
|
|
2576
|
+
}
|
|
2577
|
+
filled++;
|
|
2578
|
+
continue;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
errors2.push(`${selector}: element not found`);
|
|
2582
|
+
continue;
|
|
2583
|
+
}
|
|
2175
2584
|
if (!el) {
|
|
2176
2585
|
errors2.push(`${selector}: element not found`);
|
|
2177
2586
|
continue;
|
|
@@ -2198,11 +2607,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
2198
2607
|
if (submitSelector) {
|
|
2199
2608
|
try {
|
|
2200
2609
|
await page.click(submitSelector);
|
|
2201
|
-
} catch (
|
|
2202
|
-
|
|
2610
|
+
} catch (submitErr) {
|
|
2611
|
+
if (selfHeal) {
|
|
2612
|
+
const result = await healSelector(page, submitSelector);
|
|
2613
|
+
if (result.found && result.locator) {
|
|
2614
|
+
await result.locator.click();
|
|
2615
|
+
healedFields.push(submitSelector);
|
|
2616
|
+
} else {
|
|
2617
|
+
errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
2618
|
+
}
|
|
2619
|
+
} else {
|
|
2620
|
+
errors2.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
2621
|
+
}
|
|
2203
2622
|
}
|
|
2204
2623
|
}
|
|
2205
|
-
return { filled, errors: errors2, fields_attempted: Object.keys(fields).length };
|
|
2624
|
+
return { filled, errors: errors2, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
|
|
2206
2625
|
}
|
|
2207
2626
|
async function waitForText(page, text, opts) {
|
|
2208
2627
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -9057,6 +9476,237 @@ var init_gallery = __esm(() => {
|
|
|
9057
9476
|
init_schema();
|
|
9058
9477
|
});
|
|
9059
9478
|
|
|
9479
|
+
// src/db/recordings.ts
|
|
9480
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9481
|
+
function deserialize2(row) {
|
|
9482
|
+
return {
|
|
9483
|
+
...row,
|
|
9484
|
+
project_id: row.project_id ?? undefined,
|
|
9485
|
+
start_url: row.start_url ?? undefined,
|
|
9486
|
+
steps: JSON.parse(row.steps)
|
|
9487
|
+
};
|
|
9488
|
+
}
|
|
9489
|
+
function createRecording(data) {
|
|
9490
|
+
const db = getDatabase();
|
|
9491
|
+
const id = randomUUID5();
|
|
9492
|
+
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 ?? []));
|
|
9493
|
+
return getRecording(id);
|
|
9494
|
+
}
|
|
9495
|
+
function getRecording(id) {
|
|
9496
|
+
const db = getDatabase();
|
|
9497
|
+
const row = db.query("SELECT * FROM recordings WHERE id = ?").get(id);
|
|
9498
|
+
if (!row)
|
|
9499
|
+
throw new RecordingNotFoundError(id);
|
|
9500
|
+
return deserialize2(row);
|
|
9501
|
+
}
|
|
9502
|
+
function listRecordings(projectId) {
|
|
9503
|
+
const db = getDatabase();
|
|
9504
|
+
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();
|
|
9505
|
+
return rows.map(deserialize2);
|
|
9506
|
+
}
|
|
9507
|
+
function updateRecording(id, data) {
|
|
9508
|
+
const db = getDatabase();
|
|
9509
|
+
const fields = [];
|
|
9510
|
+
const values = [];
|
|
9511
|
+
if (data.name !== undefined) {
|
|
9512
|
+
fields.push("name = ?");
|
|
9513
|
+
values.push(data.name);
|
|
9514
|
+
}
|
|
9515
|
+
if (data.steps !== undefined) {
|
|
9516
|
+
fields.push("steps = ?");
|
|
9517
|
+
values.push(JSON.stringify(data.steps));
|
|
9518
|
+
}
|
|
9519
|
+
if (data.start_url !== undefined) {
|
|
9520
|
+
fields.push("start_url = ?");
|
|
9521
|
+
values.push(data.start_url ?? null);
|
|
9522
|
+
}
|
|
9523
|
+
if (fields.length === 0)
|
|
9524
|
+
return getRecording(id);
|
|
9525
|
+
values.push(id);
|
|
9526
|
+
db.prepare(`UPDATE recordings SET ${fields.join(", ")} WHERE id = ?`).run(...values);
|
|
9527
|
+
return getRecording(id);
|
|
9528
|
+
}
|
|
9529
|
+
var init_recordings = __esm(() => {
|
|
9530
|
+
init_schema();
|
|
9531
|
+
init_types();
|
|
9532
|
+
});
|
|
9533
|
+
|
|
9534
|
+
// src/lib/recorder.ts
|
|
9535
|
+
var exports_recorder = {};
|
|
9536
|
+
__export(exports_recorder, {
|
|
9537
|
+
stopRecording: () => stopRecording,
|
|
9538
|
+
startRecording: () => startRecording,
|
|
9539
|
+
replayRecording: () => replayRecording,
|
|
9540
|
+
recordStep: () => recordStep,
|
|
9541
|
+
listRecordings: () => listRecordings,
|
|
9542
|
+
getRecording: () => getRecording,
|
|
9543
|
+
exportRecording: () => exportRecording,
|
|
9544
|
+
attachPageListeners: () => attachPageListeners
|
|
9545
|
+
});
|
|
9546
|
+
function startRecording(sessionId, name, startUrl) {
|
|
9547
|
+
const steps = [];
|
|
9548
|
+
const recording = createRecording({ name, start_url: startUrl, steps });
|
|
9549
|
+
activeRecordings.set(recording.id, {
|
|
9550
|
+
id: recording.id,
|
|
9551
|
+
steps,
|
|
9552
|
+
cleanup: () => {}
|
|
9553
|
+
});
|
|
9554
|
+
return recording;
|
|
9555
|
+
}
|
|
9556
|
+
function attachPageListeners(page, recordingId) {
|
|
9557
|
+
const active = activeRecordings.get(recordingId);
|
|
9558
|
+
if (!active)
|
|
9559
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9560
|
+
const onFrameNav = () => {
|
|
9561
|
+
active.steps.push({
|
|
9562
|
+
type: "navigate",
|
|
9563
|
+
url: page.url(),
|
|
9564
|
+
timestamp: Date.now()
|
|
9565
|
+
});
|
|
9566
|
+
};
|
|
9567
|
+
page.on("framenavigated", onFrameNav);
|
|
9568
|
+
const cleanup = () => {
|
|
9569
|
+
page.off("framenavigated", onFrameNav);
|
|
9570
|
+
};
|
|
9571
|
+
active.cleanup = cleanup;
|
|
9572
|
+
}
|
|
9573
|
+
function recordStep(recordingId, step) {
|
|
9574
|
+
const active = activeRecordings.get(recordingId);
|
|
9575
|
+
if (!active)
|
|
9576
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9577
|
+
active.steps.push({ ...step, timestamp: Date.now() });
|
|
9578
|
+
}
|
|
9579
|
+
function stopRecording(recordingId) {
|
|
9580
|
+
const active = activeRecordings.get(recordingId);
|
|
9581
|
+
if (!active)
|
|
9582
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
9583
|
+
active.cleanup();
|
|
9584
|
+
activeRecordings.delete(recordingId);
|
|
9585
|
+
return updateRecording(recordingId, { steps: active.steps });
|
|
9586
|
+
}
|
|
9587
|
+
async function replayRecording(recordingId, page) {
|
|
9588
|
+
const recording = getRecording(recordingId);
|
|
9589
|
+
const startTime = Date.now();
|
|
9590
|
+
let executed = 0;
|
|
9591
|
+
let failed = 0;
|
|
9592
|
+
const errors2 = [];
|
|
9593
|
+
for (const step of recording.steps) {
|
|
9594
|
+
try {
|
|
9595
|
+
switch (step.type) {
|
|
9596
|
+
case "navigate":
|
|
9597
|
+
if (step.url)
|
|
9598
|
+
await navigate(page, step.url);
|
|
9599
|
+
break;
|
|
9600
|
+
case "click":
|
|
9601
|
+
if (step.selector)
|
|
9602
|
+
await click(page, step.selector);
|
|
9603
|
+
break;
|
|
9604
|
+
case "type":
|
|
9605
|
+
if (step.selector && step.value)
|
|
9606
|
+
await type(page, step.selector, step.value);
|
|
9607
|
+
break;
|
|
9608
|
+
case "scroll":
|
|
9609
|
+
await scroll(page, "down");
|
|
9610
|
+
break;
|
|
9611
|
+
case "hover":
|
|
9612
|
+
if (step.selector) {
|
|
9613
|
+
const el = await page.$(step.selector);
|
|
9614
|
+
if (el)
|
|
9615
|
+
await el.hover();
|
|
9616
|
+
}
|
|
9617
|
+
break;
|
|
9618
|
+
case "evaluate":
|
|
9619
|
+
if (step.value)
|
|
9620
|
+
await page.evaluate(step.value);
|
|
9621
|
+
break;
|
|
9622
|
+
case "wait":
|
|
9623
|
+
if (step.selector) {
|
|
9624
|
+
await page.waitForSelector(step.selector, { timeout: 1e4 }).catch(() => {});
|
|
9625
|
+
}
|
|
9626
|
+
break;
|
|
9627
|
+
}
|
|
9628
|
+
executed++;
|
|
9629
|
+
} catch (err) {
|
|
9630
|
+
failed++;
|
|
9631
|
+
errors2.push(`Step ${step.type} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
9632
|
+
}
|
|
9633
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
9634
|
+
}
|
|
9635
|
+
return {
|
|
9636
|
+
recording_id: recordingId,
|
|
9637
|
+
success: failed === 0,
|
|
9638
|
+
steps_executed: executed,
|
|
9639
|
+
steps_failed: failed,
|
|
9640
|
+
errors: errors2,
|
|
9641
|
+
duration_ms: Date.now() - startTime
|
|
9642
|
+
};
|
|
9643
|
+
}
|
|
9644
|
+
function exportRecording(recordingId, format = "json") {
|
|
9645
|
+
const recording = getRecording(recordingId);
|
|
9646
|
+
if (format === "json") {
|
|
9647
|
+
return JSON.stringify(recording, null, 2);
|
|
9648
|
+
}
|
|
9649
|
+
if (format === "playwright") {
|
|
9650
|
+
const lines2 = [
|
|
9651
|
+
`import { test, expect } from '@playwright/test';`,
|
|
9652
|
+
``,
|
|
9653
|
+
`test('${recording.name}', async ({ page }) => {`
|
|
9654
|
+
];
|
|
9655
|
+
for (const step of recording.steps) {
|
|
9656
|
+
switch (step.type) {
|
|
9657
|
+
case "navigate":
|
|
9658
|
+
lines2.push(` await page.goto('${step.url}');`);
|
|
9659
|
+
break;
|
|
9660
|
+
case "click":
|
|
9661
|
+
lines2.push(` await page.click('${step.selector}');`);
|
|
9662
|
+
break;
|
|
9663
|
+
case "type":
|
|
9664
|
+
lines2.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
9665
|
+
break;
|
|
9666
|
+
case "scroll":
|
|
9667
|
+
lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
|
|
9668
|
+
break;
|
|
9669
|
+
case "evaluate":
|
|
9670
|
+
lines2.push(` await page.evaluate(${step.value});`);
|
|
9671
|
+
break;
|
|
9672
|
+
}
|
|
9673
|
+
}
|
|
9674
|
+
lines2.push(`});`);
|
|
9675
|
+
return lines2.join(`
|
|
9676
|
+
`);
|
|
9677
|
+
}
|
|
9678
|
+
const lines = [
|
|
9679
|
+
`const puppeteer = require('puppeteer');`,
|
|
9680
|
+
``,
|
|
9681
|
+
`(async () => {`,
|
|
9682
|
+
` const browser = await puppeteer.launch();`,
|
|
9683
|
+
` const page = await browser.newPage();`
|
|
9684
|
+
];
|
|
9685
|
+
for (const step of recording.steps) {
|
|
9686
|
+
switch (step.type) {
|
|
9687
|
+
case "navigate":
|
|
9688
|
+
lines.push(` await page.goto('${step.url}');`);
|
|
9689
|
+
break;
|
|
9690
|
+
case "click":
|
|
9691
|
+
lines.push(` await page.click('${step.selector}');`);
|
|
9692
|
+
break;
|
|
9693
|
+
case "type":
|
|
9694
|
+
lines.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
9695
|
+
break;
|
|
9696
|
+
}
|
|
9697
|
+
}
|
|
9698
|
+
lines.push(` await browser.close();`, `})();`);
|
|
9699
|
+
return lines.join(`
|
|
9700
|
+
`);
|
|
9701
|
+
}
|
|
9702
|
+
var activeRecordings;
|
|
9703
|
+
var init_recorder = __esm(() => {
|
|
9704
|
+
init_recordings();
|
|
9705
|
+
init_actions();
|
|
9706
|
+
init_types();
|
|
9707
|
+
activeRecordings = new Map;
|
|
9708
|
+
});
|
|
9709
|
+
|
|
9060
9710
|
// src/lib/profiles.ts
|
|
9061
9711
|
var exports_profiles = {};
|
|
9062
9712
|
__export(exports_profiles, {
|
|
@@ -9066,23 +9716,23 @@ __export(exports_profiles, {
|
|
|
9066
9716
|
deleteProfile: () => deleteProfile,
|
|
9067
9717
|
applyProfile: () => applyProfile
|
|
9068
9718
|
});
|
|
9069
|
-
import { mkdirSync as
|
|
9070
|
-
import { join as
|
|
9071
|
-
import { homedir as
|
|
9719
|
+
import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
9720
|
+
import { join as join8 } from "path";
|
|
9721
|
+
import { homedir as homedir8 } from "os";
|
|
9072
9722
|
function getProfilesDir() {
|
|
9073
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
9074
|
-
const dir =
|
|
9075
|
-
|
|
9723
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
|
|
9724
|
+
const dir = join8(dataDir, "profiles");
|
|
9725
|
+
mkdirSync8(dir, { recursive: true });
|
|
9076
9726
|
return dir;
|
|
9077
9727
|
}
|
|
9078
9728
|
function getProfileDir2(name) {
|
|
9079
|
-
return
|
|
9729
|
+
return join8(getProfilesDir(), name);
|
|
9080
9730
|
}
|
|
9081
9731
|
async function saveProfile(page, name) {
|
|
9082
9732
|
const dir = getProfileDir2(name);
|
|
9083
|
-
|
|
9733
|
+
mkdirSync8(dir, { recursive: true });
|
|
9084
9734
|
const cookies = await page.context().cookies();
|
|
9085
|
-
writeFileSync2(
|
|
9735
|
+
writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
9086
9736
|
let localStorage2 = {};
|
|
9087
9737
|
try {
|
|
9088
9738
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -9094,11 +9744,11 @@ async function saveProfile(page, name) {
|
|
|
9094
9744
|
return result;
|
|
9095
9745
|
});
|
|
9096
9746
|
} catch {}
|
|
9097
|
-
writeFileSync2(
|
|
9747
|
+
writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
9098
9748
|
const savedAt = new Date().toISOString();
|
|
9099
9749
|
const url = page.url();
|
|
9100
9750
|
const meta = { saved_at: savedAt, url };
|
|
9101
|
-
writeFileSync2(
|
|
9751
|
+
writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
9102
9752
|
return {
|
|
9103
9753
|
name,
|
|
9104
9754
|
saved_at: savedAt,
|
|
@@ -9109,17 +9759,17 @@ async function saveProfile(page, name) {
|
|
|
9109
9759
|
}
|
|
9110
9760
|
function loadProfile(name) {
|
|
9111
9761
|
const dir = getProfileDir2(name);
|
|
9112
|
-
if (!
|
|
9762
|
+
if (!existsSync4(dir)) {
|
|
9113
9763
|
throw new Error(`Profile not found: ${name}`);
|
|
9114
9764
|
}
|
|
9115
|
-
const cookiesPath =
|
|
9116
|
-
const storagePath =
|
|
9117
|
-
const metaPath2 =
|
|
9118
|
-
const cookies =
|
|
9119
|
-
const localStorage2 =
|
|
9765
|
+
const cookiesPath = join8(dir, "cookies.json");
|
|
9766
|
+
const storagePath = join8(dir, "storage.json");
|
|
9767
|
+
const metaPath2 = join8(dir, "meta.json");
|
|
9768
|
+
const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
9769
|
+
const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
9120
9770
|
let savedAt = new Date().toISOString();
|
|
9121
9771
|
let url;
|
|
9122
|
-
if (
|
|
9772
|
+
if (existsSync4(metaPath2)) {
|
|
9123
9773
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
9124
9774
|
savedAt = meta.saved_at ?? savedAt;
|
|
9125
9775
|
url = meta.url;
|
|
@@ -9147,33 +9797,33 @@ async function applyProfile(page, profileData) {
|
|
|
9147
9797
|
}
|
|
9148
9798
|
function listProfiles() {
|
|
9149
9799
|
const dir = getProfilesDir();
|
|
9150
|
-
if (!
|
|
9800
|
+
if (!existsSync4(dir))
|
|
9151
9801
|
return [];
|
|
9152
|
-
const entries =
|
|
9802
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
9153
9803
|
const profiles = [];
|
|
9154
9804
|
for (const entry of entries) {
|
|
9155
9805
|
if (!entry.isDirectory())
|
|
9156
9806
|
continue;
|
|
9157
9807
|
const name = entry.name;
|
|
9158
|
-
const profileDir =
|
|
9808
|
+
const profileDir = join8(dir, name);
|
|
9159
9809
|
let savedAt = "";
|
|
9160
9810
|
let url;
|
|
9161
9811
|
let cookieCount = 0;
|
|
9162
9812
|
let storageKeyCount = 0;
|
|
9163
9813
|
try {
|
|
9164
|
-
const metaPath2 =
|
|
9165
|
-
if (
|
|
9814
|
+
const metaPath2 = join8(profileDir, "meta.json");
|
|
9815
|
+
if (existsSync4(metaPath2)) {
|
|
9166
9816
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
9167
9817
|
savedAt = meta.saved_at ?? "";
|
|
9168
9818
|
url = meta.url;
|
|
9169
9819
|
}
|
|
9170
|
-
const cookiesPath =
|
|
9171
|
-
if (
|
|
9820
|
+
const cookiesPath = join8(profileDir, "cookies.json");
|
|
9821
|
+
if (existsSync4(cookiesPath)) {
|
|
9172
9822
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
9173
9823
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
9174
9824
|
}
|
|
9175
|
-
const storagePath =
|
|
9176
|
-
if (
|
|
9825
|
+
const storagePath = join8(profileDir, "storage.json");
|
|
9826
|
+
if (existsSync4(storagePath)) {
|
|
9177
9827
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
9178
9828
|
storageKeyCount = Object.keys(storage).length;
|
|
9179
9829
|
}
|
|
@@ -9190,7 +9840,7 @@ function listProfiles() {
|
|
|
9190
9840
|
}
|
|
9191
9841
|
function deleteProfile(name) {
|
|
9192
9842
|
const dir = getProfileDir2(name);
|
|
9193
|
-
if (!
|
|
9843
|
+
if (!existsSync4(dir))
|
|
9194
9844
|
return false;
|
|
9195
9845
|
try {
|
|
9196
9846
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -9201,6 +9851,97 @@ function deleteProfile(name) {
|
|
|
9201
9851
|
}
|
|
9202
9852
|
var init_profiles = () => {};
|
|
9203
9853
|
|
|
9854
|
+
// src/lib/sanitize.ts
|
|
9855
|
+
var exports_sanitize = {};
|
|
9856
|
+
__export(exports_sanitize, {
|
|
9857
|
+
sanitizeText: () => sanitizeText,
|
|
9858
|
+
sanitizeHTML: () => sanitizeHTML
|
|
9859
|
+
});
|
|
9860
|
+
function sanitizeText(text) {
|
|
9861
|
+
let stripped = 0;
|
|
9862
|
+
const warnings = [];
|
|
9863
|
+
let clean = text;
|
|
9864
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
9865
|
+
pattern.lastIndex = 0;
|
|
9866
|
+
const matches = clean.match(pattern);
|
|
9867
|
+
if (matches) {
|
|
9868
|
+
stripped += matches.length;
|
|
9869
|
+
warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
|
|
9870
|
+
pattern.lastIndex = 0;
|
|
9871
|
+
clean = clean.replace(pattern, "[STRIPPED]");
|
|
9872
|
+
}
|
|
9873
|
+
}
|
|
9874
|
+
return { text: clean, stripped, warnings };
|
|
9875
|
+
}
|
|
9876
|
+
function sanitizeHTML(html) {
|
|
9877
|
+
let stripped = 0;
|
|
9878
|
+
const warnings = [];
|
|
9879
|
+
let clean = html;
|
|
9880
|
+
const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
|
|
9881
|
+
if (commentMatches) {
|
|
9882
|
+
for (const comment of commentMatches) {
|
|
9883
|
+
if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
|
|
9884
|
+
stripped++;
|
|
9885
|
+
warnings.push(`Stripped HTML comment (${comment.length} chars)`);
|
|
9886
|
+
}
|
|
9887
|
+
}
|
|
9888
|
+
clean = clean.replace(/<!--[\s\S]*?-->/g, "");
|
|
9889
|
+
}
|
|
9890
|
+
const hiddenPatterns = [
|
|
9891
|
+
/style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9892
|
+
/style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9893
|
+
/style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9894
|
+
/style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
9895
|
+
/style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
|
|
9896
|
+
];
|
|
9897
|
+
for (const pattern of hiddenPatterns) {
|
|
9898
|
+
pattern.lastIndex = 0;
|
|
9899
|
+
const matches = clean.match(pattern);
|
|
9900
|
+
if (matches) {
|
|
9901
|
+
stripped += matches.length;
|
|
9902
|
+
warnings.push(`Stripped ${matches.length} hidden elements`);
|
|
9903
|
+
pattern.lastIndex = 0;
|
|
9904
|
+
clean = clean.replace(pattern, "");
|
|
9905
|
+
}
|
|
9906
|
+
}
|
|
9907
|
+
const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
|
|
9908
|
+
const ariaHidden = clean.match(ariaHiddenPattern);
|
|
9909
|
+
if (ariaHidden) {
|
|
9910
|
+
stripped += ariaHidden.length;
|
|
9911
|
+
warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
|
|
9912
|
+
ariaHiddenPattern.lastIndex = 0;
|
|
9913
|
+
clean = clean.replace(ariaHiddenPattern, "");
|
|
9914
|
+
}
|
|
9915
|
+
const textResult = sanitizeText(clean);
|
|
9916
|
+
return {
|
|
9917
|
+
text: textResult.text,
|
|
9918
|
+
stripped: stripped + textResult.stripped,
|
|
9919
|
+
warnings: [...warnings, ...textResult.warnings]
|
|
9920
|
+
};
|
|
9921
|
+
}
|
|
9922
|
+
var INJECTION_PATTERNS;
|
|
9923
|
+
var init_sanitize = __esm(() => {
|
|
9924
|
+
INJECTION_PATTERNS = [
|
|
9925
|
+
/ignore\s+(all\s+)?previous\s+instructions/gi,
|
|
9926
|
+
/ignore\s+(all\s+)?prior\s+instructions/gi,
|
|
9927
|
+
/disregard\s+(all\s+)?previous/gi,
|
|
9928
|
+
/forget\s+(all\s+)?previous/gi,
|
|
9929
|
+
/you\s+are\s+now\s+/gi,
|
|
9930
|
+
/new\s+instructions?\s*:/gi,
|
|
9931
|
+
/system\s+prompt\s*:/gi,
|
|
9932
|
+
/\[INST\]/gi,
|
|
9933
|
+
/\[\/INST\]/gi,
|
|
9934
|
+
/<\|im_start\|>/gi,
|
|
9935
|
+
/<\|im_end\|>/gi,
|
|
9936
|
+
/<<SYS>>/gi,
|
|
9937
|
+
/<<\/SYS>>/gi,
|
|
9938
|
+
/IMPORTANT:\s*ignore/gi,
|
|
9939
|
+
/CRITICAL:\s*override/gi,
|
|
9940
|
+
/assistant:\s/gi,
|
|
9941
|
+
/human:\s/gi
|
|
9942
|
+
];
|
|
9943
|
+
});
|
|
9944
|
+
|
|
9204
9945
|
// src/lib/annotate.ts
|
|
9205
9946
|
var exports_annotate = {};
|
|
9206
9947
|
__export(exports_annotate, {
|
|
@@ -9261,26 +10002,846 @@ var init_annotate = __esm(() => {
|
|
|
9261
10002
|
import_sharp3 = __toESM(require_lib(), 1);
|
|
9262
10003
|
});
|
|
9263
10004
|
|
|
10005
|
+
// src/lib/env-detector.ts
|
|
10006
|
+
var exports_env_detector = {};
|
|
10007
|
+
__export(exports_env_detector, {
|
|
10008
|
+
detectEnvironment: () => detectEnvironment
|
|
10009
|
+
});
|
|
10010
|
+
async function detectEnvironment(page) {
|
|
10011
|
+
const url = page.url();
|
|
10012
|
+
const signals = [];
|
|
10013
|
+
let score = { local: 0, dev: 0, staging: 0, prod: 0 };
|
|
10014
|
+
try {
|
|
10015
|
+
const u = new URL(url);
|
|
10016
|
+
if (u.hostname === "localhost" || u.hostname === "127.0.0.1" || u.hostname === "0.0.0.0" || u.hostname.endsWith(".local")) {
|
|
10017
|
+
score.local += 5;
|
|
10018
|
+
signals.push(`URL hostname: ${u.hostname} \u2192 local`);
|
|
10019
|
+
} else if (u.hostname.match(/^(dev|development)\./i) || u.port !== "") {
|
|
10020
|
+
score.dev += 4;
|
|
10021
|
+
signals.push(`URL pattern: ${u.hostname}:${u.port} \u2192 dev`);
|
|
10022
|
+
} else if (u.hostname.match(/^(staging|stg|stage|preprod|uat)\./i)) {
|
|
10023
|
+
score.staging += 4;
|
|
10024
|
+
signals.push(`URL pattern: ${u.hostname} \u2192 staging`);
|
|
10025
|
+
} else {
|
|
10026
|
+
score.prod += 2;
|
|
10027
|
+
signals.push(`URL looks production: ${u.hostname}`);
|
|
10028
|
+
}
|
|
10029
|
+
if (u.port && !["80", "443", ""].includes(u.port)) {
|
|
10030
|
+
score.dev += 2;
|
|
10031
|
+
signals.push(`Non-standard port: ${u.port}`);
|
|
10032
|
+
}
|
|
10033
|
+
if (u.protocol === "https:") {
|
|
10034
|
+
score.prod += 1;
|
|
10035
|
+
signals.push("HTTPS \u2192 likely prod");
|
|
10036
|
+
} else {
|
|
10037
|
+
score.dev += 2;
|
|
10038
|
+
signals.push("HTTP \u2192 likely dev/local");
|
|
10039
|
+
}
|
|
10040
|
+
} catch {}
|
|
10041
|
+
try {
|
|
10042
|
+
const pageSignals = await page.evaluate(() => {
|
|
10043
|
+
const s = [];
|
|
10044
|
+
const w = window;
|
|
10045
|
+
const envVars = ["__ENV__", "__NEXT_DATA__", "__NUXT__", "process"];
|
|
10046
|
+
for (const v of envVars) {
|
|
10047
|
+
if (w[v]) {
|
|
10048
|
+
const env2 = w[v]?.env?.NODE_ENV ?? w[v]?.runtimeConfig?.public?.env ?? w[v]?.props?.pageProps?.env;
|
|
10049
|
+
if (env2)
|
|
10050
|
+
s.push(`window.${v}: ${env2}`);
|
|
10051
|
+
}
|
|
10052
|
+
}
|
|
10053
|
+
if (w.__REACT_DEVTOOLS_GLOBAL_HOOK__?.renderers?.size > 0) {
|
|
10054
|
+
const fiber = document.querySelector("[data-reactroot]") || document.getElementById("__next") || document.getElementById("root");
|
|
10055
|
+
if (fiber)
|
|
10056
|
+
s.push("React app detected");
|
|
10057
|
+
}
|
|
10058
|
+
const envMeta = document.querySelector('meta[name="environment"], meta[name="env"], meta[name="deploy-env"]');
|
|
10059
|
+
if (envMeta)
|
|
10060
|
+
s.push(`meta[environment]: ${envMeta.getAttribute("content")}`);
|
|
10061
|
+
const scripts = document.querySelectorAll("script[src]");
|
|
10062
|
+
let minified = 0, unminified = 0;
|
|
10063
|
+
scripts.forEach((s2) => {
|
|
10064
|
+
const src = s2.getAttribute("src") ?? "";
|
|
10065
|
+
if (src.includes(".min.") || src.match(/\.[a-f0-9]{8,}\./))
|
|
10066
|
+
minified++;
|
|
10067
|
+
else if (src.endsWith(".js") && !src.includes("chunk"))
|
|
10068
|
+
unminified++;
|
|
10069
|
+
});
|
|
10070
|
+
if (unminified > minified && unminified > 2)
|
|
10071
|
+
s.push(`Unminified scripts (${unminified}/${minified + unminified}) \u2192 likely dev`);
|
|
10072
|
+
else if (minified > 0)
|
|
10073
|
+
s.push(`Minified/hashed scripts (${minified}/${minified + unminified}) \u2192 likely prod`);
|
|
10074
|
+
if (document.querySelector("[data-testid]"))
|
|
10075
|
+
s.push("data-testid attributes present \u2192 dev/staging");
|
|
10076
|
+
if ("serviceWorker" in navigator && navigator.serviceWorker.controller) {
|
|
10077
|
+
s.push("Service worker active \u2192 likely prod");
|
|
10078
|
+
}
|
|
10079
|
+
if (w.Sentry)
|
|
10080
|
+
s.push("Sentry SDK loaded \u2192 prod monitoring");
|
|
10081
|
+
if (w.__DATADOG_SYNTHETICS_INLINED_SCRIPT)
|
|
10082
|
+
s.push("Datadog loaded \u2192 prod monitoring");
|
|
10083
|
+
if (w.LogRocket)
|
|
10084
|
+
s.push("LogRocket loaded \u2192 prod monitoring");
|
|
10085
|
+
if (w._lr_loaded)
|
|
10086
|
+
s.push("LogRocket loaded \u2192 prod monitoring");
|
|
10087
|
+
if (w.gtag || w.ga)
|
|
10088
|
+
s.push("Google Analytics loaded \u2192 likely prod");
|
|
10089
|
+
if (w.posthog || w._ph)
|
|
10090
|
+
s.push("PostHog loaded \u2192 prod analytics");
|
|
10091
|
+
if (w.mixpanel)
|
|
10092
|
+
s.push("Mixpanel loaded \u2192 prod analytics");
|
|
10093
|
+
const robots = document.querySelector('meta[name="robots"]');
|
|
10094
|
+
if (robots) {
|
|
10095
|
+
const content = robots.getAttribute("content") ?? "";
|
|
10096
|
+
if (content.includes("noindex"))
|
|
10097
|
+
s.push(`robots: noindex \u2192 staging/dev`);
|
|
10098
|
+
}
|
|
10099
|
+
return s;
|
|
10100
|
+
});
|
|
10101
|
+
for (const signal of pageSignals) {
|
|
10102
|
+
signals.push(signal);
|
|
10103
|
+
if (signal.includes("development") || signal.includes("\u2192 dev") || signal.includes("\u2192 likely dev"))
|
|
10104
|
+
score.dev += 2;
|
|
10105
|
+
if (signal.includes("production") || signal.includes("\u2192 prod") || signal.includes("\u2192 likely prod"))
|
|
10106
|
+
score.prod += 2;
|
|
10107
|
+
if (signal.includes("staging") || signal.includes("\u2192 staging"))
|
|
10108
|
+
score.staging += 2;
|
|
10109
|
+
if (signal.includes("monitoring") || signal.includes("analytics"))
|
|
10110
|
+
score.prod += 1;
|
|
10111
|
+
if (signal.includes("noindex")) {
|
|
10112
|
+
score.staging += 2;
|
|
10113
|
+
score.dev += 1;
|
|
10114
|
+
}
|
|
10115
|
+
}
|
|
10116
|
+
} catch {}
|
|
10117
|
+
const entries = Object.entries(score);
|
|
10118
|
+
entries.sort((a, b) => b[1] - a[1]);
|
|
10119
|
+
const [env, topScore] = entries[0];
|
|
10120
|
+
const [, secondScore] = entries[1];
|
|
10121
|
+
const confidence = topScore >= 5 ? "high" : topScore > secondScore + 1 ? "medium" : "low";
|
|
10122
|
+
return { env, confidence, signals };
|
|
10123
|
+
}
|
|
10124
|
+
|
|
10125
|
+
// src/lib/deep-performance.ts
|
|
10126
|
+
var exports_deep_performance = {};
|
|
10127
|
+
__export(exports_deep_performance, {
|
|
10128
|
+
getDeepPerformance: () => getDeepPerformance
|
|
10129
|
+
});
|
|
10130
|
+
function categorizeThirdParty(domain) {
|
|
10131
|
+
for (const [pattern, category] of Object.entries(THIRD_PARTY_CATEGORIES)) {
|
|
10132
|
+
if (domain.includes(pattern))
|
|
10133
|
+
return category;
|
|
10134
|
+
}
|
|
10135
|
+
return "other";
|
|
10136
|
+
}
|
|
10137
|
+
async function getDeepPerformance(page) {
|
|
10138
|
+
return page.evaluate(() => {
|
|
10139
|
+
const perf = performance;
|
|
10140
|
+
const entries = perf.getEntriesByType("resource");
|
|
10141
|
+
const navEntry = perf.getEntriesByType("navigation")[0];
|
|
10142
|
+
const paintEntries = perf.getEntriesByType("paint");
|
|
10143
|
+
const fcp = paintEntries.find((e) => e.name === "first-contentful-paint")?.startTime;
|
|
10144
|
+
const ttfb = navEntry?.responseStart;
|
|
10145
|
+
const web_vitals = { fcp, ttfb };
|
|
10146
|
+
try {
|
|
10147
|
+
const lcpEntries = perf.getEntriesByType("largest-contentful-paint");
|
|
10148
|
+
if (lcpEntries.length > 0)
|
|
10149
|
+
web_vitals.lcp = lcpEntries[lcpEntries.length - 1].startTime;
|
|
10150
|
+
} catch {}
|
|
10151
|
+
const byType = {};
|
|
10152
|
+
let totalBytes = 0;
|
|
10153
|
+
const resourceList = [];
|
|
10154
|
+
const pageDomain = location.hostname;
|
|
10155
|
+
const thirdPartyMap = new Map;
|
|
10156
|
+
for (const entry of entries) {
|
|
10157
|
+
const size = entry.transferSize || entry.encodedBodySize || 0;
|
|
10158
|
+
totalBytes += size;
|
|
10159
|
+
let type2 = entry.initiatorType || "other";
|
|
10160
|
+
if (type2 === "xmlhttprequest" || type2 === "fetch")
|
|
10161
|
+
type2 = "xhr";
|
|
10162
|
+
if (type2 === "link" && entry.name.match(/\.css/))
|
|
10163
|
+
type2 = "css";
|
|
10164
|
+
if (type2 === "img" || entry.name.match(/\.(png|jpg|jpeg|gif|svg|webp|avif|ico)/i))
|
|
10165
|
+
type2 = "image";
|
|
10166
|
+
if (type2 === "script" || entry.name.match(/\.js/))
|
|
10167
|
+
type2 = "script";
|
|
10168
|
+
if (entry.name.match(/\.(woff2?|ttf|otf|eot)/i))
|
|
10169
|
+
type2 = "font";
|
|
10170
|
+
if (!byType[type2])
|
|
10171
|
+
byType[type2] = { count: 0, size_bytes: 0 };
|
|
10172
|
+
byType[type2].count++;
|
|
10173
|
+
byType[type2].size_bytes += size;
|
|
10174
|
+
resourceList.push({ url: entry.name, size_bytes: size, type: type2 });
|
|
10175
|
+
try {
|
|
10176
|
+
const domain = new URL(entry.name).hostname;
|
|
10177
|
+
if (domain !== pageDomain && !domain.endsWith(`.${pageDomain}`)) {
|
|
10178
|
+
if (!thirdPartyMap.has(domain))
|
|
10179
|
+
thirdPartyMap.set(domain, { scripts: 0, total_bytes: 0 });
|
|
10180
|
+
const tp = thirdPartyMap.get(domain);
|
|
10181
|
+
tp.scripts++;
|
|
10182
|
+
tp.total_bytes += size;
|
|
10183
|
+
}
|
|
10184
|
+
} catch {}
|
|
10185
|
+
}
|
|
10186
|
+
resourceList.sort((a, b) => b.size_bytes - a.size_bytes);
|
|
10187
|
+
const largest = resourceList.slice(0, 10).map((r) => ({
|
|
10188
|
+
url: r.url.length > 120 ? r.url.slice(0, 117) + "..." : r.url,
|
|
10189
|
+
size_bytes: r.size_bytes,
|
|
10190
|
+
type: r.type
|
|
10191
|
+
}));
|
|
10192
|
+
const third_party = Array.from(thirdPartyMap.entries()).map(([domain, data]) => ({ domain, ...data, category: "" })).sort((a, b) => b.total_bytes - a.total_bytes).slice(0, 15);
|
|
10193
|
+
const allNodes = document.querySelectorAll("*");
|
|
10194
|
+
let maxDepth = 0;
|
|
10195
|
+
let textNodes = 0;
|
|
10196
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ALL);
|
|
10197
|
+
let node = walker.currentNode;
|
|
10198
|
+
while (node) {
|
|
10199
|
+
if (node.nodeType === Node.TEXT_NODE && node.textContent?.trim())
|
|
10200
|
+
textNodes++;
|
|
10201
|
+
let depth = 0;
|
|
10202
|
+
let parent = node.parentNode;
|
|
10203
|
+
while (parent) {
|
|
10204
|
+
depth++;
|
|
10205
|
+
parent = parent.parentNode;
|
|
10206
|
+
}
|
|
10207
|
+
if (depth > maxDepth)
|
|
10208
|
+
maxDepth = depth;
|
|
10209
|
+
node = walker.nextNode();
|
|
10210
|
+
}
|
|
10211
|
+
const mem = performance.memory;
|
|
10212
|
+
const memory = {
|
|
10213
|
+
js_heap_used_mb: mem ? Math.round(mem.usedJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
|
|
10214
|
+
js_heap_total_mb: mem ? Math.round(mem.totalJSHeapSize / 1024 / 1024 * 100) / 100 : 0,
|
|
10215
|
+
js_heap_limit_mb: mem ? Math.round(mem.jsHeapSizeLimit / 1024 / 1024 * 100) / 100 : 0
|
|
10216
|
+
};
|
|
10217
|
+
return {
|
|
10218
|
+
web_vitals,
|
|
10219
|
+
resources: { total_transfer_bytes: totalBytes, total_resources: entries.length, by_type: byType, largest },
|
|
10220
|
+
third_party,
|
|
10221
|
+
dom: { node_count: document.all.length, max_depth: maxDepth, element_count: allNodes.length, text_node_count: textNodes },
|
|
10222
|
+
main_thread: { long_tasks: 0, total_blocking_ms: 0 },
|
|
10223
|
+
memory
|
|
10224
|
+
};
|
|
10225
|
+
}).then((result) => {
|
|
10226
|
+
for (const tp of result.third_party) {
|
|
10227
|
+
tp.category = categorizeThirdParty(tp.domain);
|
|
10228
|
+
}
|
|
10229
|
+
return result;
|
|
10230
|
+
});
|
|
10231
|
+
}
|
|
10232
|
+
var THIRD_PARTY_CATEGORIES;
|
|
10233
|
+
var init_deep_performance = __esm(() => {
|
|
10234
|
+
THIRD_PARTY_CATEGORIES = {
|
|
10235
|
+
"google-analytics.com": "analytics",
|
|
10236
|
+
"googletagmanager.com": "analytics",
|
|
10237
|
+
gtag: "analytics",
|
|
10238
|
+
"facebook.net": "social",
|
|
10239
|
+
"connect.facebook": "social",
|
|
10240
|
+
"stripe.com": "payment",
|
|
10241
|
+
"js.stripe.com": "payment",
|
|
10242
|
+
"sentry.io": "monitoring",
|
|
10243
|
+
"sentry-cdn": "monitoring",
|
|
10244
|
+
"posthog.com": "analytics",
|
|
10245
|
+
"ph.": "analytics",
|
|
10246
|
+
"intercom.io": "chat",
|
|
10247
|
+
"crisp.chat": "chat",
|
|
10248
|
+
"hotjar.com": "analytics",
|
|
10249
|
+
"clarity.ms": "analytics",
|
|
10250
|
+
"cdn.jsdelivr.net": "cdn",
|
|
10251
|
+
"cdnjs.cloudflare.com": "cdn",
|
|
10252
|
+
"unpkg.com": "cdn",
|
|
10253
|
+
"fonts.googleapis.com": "fonts",
|
|
10254
|
+
"fonts.gstatic.com": "fonts"
|
|
10255
|
+
};
|
|
10256
|
+
});
|
|
10257
|
+
|
|
10258
|
+
// src/lib/workflows.ts
|
|
10259
|
+
var exports_workflows = {};
|
|
10260
|
+
__export(exports_workflows, {
|
|
10261
|
+
saveWorkflowFromRecording: () => saveWorkflowFromRecording,
|
|
10262
|
+
saveWorkflow: () => saveWorkflow,
|
|
10263
|
+
runWorkflow: () => runWorkflow,
|
|
10264
|
+
listWorkflows: () => listWorkflows,
|
|
10265
|
+
getWorkflowByName: () => getWorkflowByName,
|
|
10266
|
+
getWorkflow: () => getWorkflow,
|
|
10267
|
+
deleteWorkflow: () => deleteWorkflow
|
|
10268
|
+
});
|
|
10269
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
10270
|
+
function saveWorkflow(data) {
|
|
10271
|
+
const db = getDatabase();
|
|
10272
|
+
const id = randomUUID10();
|
|
10273
|
+
db.prepare("INSERT OR REPLACE INTO workflows (id, name, description, steps, start_url) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.description ?? null, JSON.stringify(data.steps), data.startUrl ?? null);
|
|
10274
|
+
return getWorkflow(id);
|
|
10275
|
+
}
|
|
10276
|
+
function saveWorkflowFromRecording(recordingId, name, description) {
|
|
10277
|
+
const db = getDatabase();
|
|
10278
|
+
const rec = db.query("SELECT steps, start_url FROM recordings WHERE id = ?").get(recordingId);
|
|
10279
|
+
if (!rec)
|
|
10280
|
+
throw new Error(`Recording not found: ${recordingId}`);
|
|
10281
|
+
const steps = JSON.parse(rec.steps);
|
|
10282
|
+
return saveWorkflow({ name, description, steps, startUrl: rec.start_url ?? undefined });
|
|
10283
|
+
}
|
|
10284
|
+
function getWorkflow(id) {
|
|
10285
|
+
const db = getDatabase();
|
|
10286
|
+
const row = db.query("SELECT * FROM workflows WHERE id = ?").get(id);
|
|
10287
|
+
if (!row)
|
|
10288
|
+
return null;
|
|
10289
|
+
return { ...row, steps: JSON.parse(row.steps) };
|
|
10290
|
+
}
|
|
10291
|
+
function getWorkflowByName(name) {
|
|
10292
|
+
const db = getDatabase();
|
|
10293
|
+
const row = db.query("SELECT * FROM workflows WHERE name = ?").get(name);
|
|
10294
|
+
if (!row)
|
|
10295
|
+
return null;
|
|
10296
|
+
return { ...row, steps: JSON.parse(row.steps) };
|
|
10297
|
+
}
|
|
10298
|
+
function listWorkflows() {
|
|
10299
|
+
const db = getDatabase();
|
|
10300
|
+
return db.query("SELECT * FROM workflows ORDER BY updated_at DESC").all().map((row) => ({ ...row, steps: JSON.parse(row.steps) }));
|
|
10301
|
+
}
|
|
10302
|
+
function deleteWorkflow(name) {
|
|
10303
|
+
const db = getDatabase();
|
|
10304
|
+
return db.prepare("DELETE FROM workflows WHERE name = ?").run(name).changes > 0;
|
|
10305
|
+
}
|
|
10306
|
+
function recordRun(id, healed) {
|
|
10307
|
+
const db = getDatabase();
|
|
10308
|
+
if (healed) {
|
|
10309
|
+
db.prepare("UPDATE workflows SET last_run = datetime('now'), last_heal = datetime('now'), heal_count = heal_count + 1, run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
10310
|
+
} else {
|
|
10311
|
+
db.prepare("UPDATE workflows SET last_run = datetime('now'), run_count = run_count + 1, updated_at = datetime('now') WHERE id = ?").run(id);
|
|
10312
|
+
}
|
|
10313
|
+
}
|
|
10314
|
+
async function runWorkflow(workflow, page) {
|
|
10315
|
+
const t0 = Date.now();
|
|
10316
|
+
let executed = 0;
|
|
10317
|
+
let failed = 0;
|
|
10318
|
+
let healed = 0;
|
|
10319
|
+
const healedDetails = [];
|
|
10320
|
+
const errors2 = [];
|
|
10321
|
+
const updatedSteps = [...workflow.steps];
|
|
10322
|
+
for (let i = 0;i < workflow.steps.length; i++) {
|
|
10323
|
+
const step = workflow.steps[i];
|
|
10324
|
+
try {
|
|
10325
|
+
switch (step.type) {
|
|
10326
|
+
case "navigate":
|
|
10327
|
+
if (step.url)
|
|
10328
|
+
await page.goto(step.url, { waitUntil: "domcontentloaded", timeout: 30000 });
|
|
10329
|
+
break;
|
|
10330
|
+
case "click":
|
|
10331
|
+
if (step.selector) {
|
|
10332
|
+
try {
|
|
10333
|
+
await page.click(step.selector, { timeout: 5000 });
|
|
10334
|
+
} catch {
|
|
10335
|
+
const result = await healSelector(page, step.selector);
|
|
10336
|
+
if (result.found && result.locator) {
|
|
10337
|
+
await result.locator.click();
|
|
10338
|
+
healed++;
|
|
10339
|
+
const healedSelector = `[healed:${result.method}]${step.selector}`;
|
|
10340
|
+
healedDetails.push({ step: i, original: step.selector, healed_to: healedSelector, method: result.method });
|
|
10341
|
+
} else {
|
|
10342
|
+
throw new Error(`Click failed: ${step.selector} (self-healing exhausted)`);
|
|
10343
|
+
}
|
|
10344
|
+
}
|
|
10345
|
+
}
|
|
10346
|
+
break;
|
|
10347
|
+
case "type":
|
|
10348
|
+
if (step.selector && step.value) {
|
|
10349
|
+
try {
|
|
10350
|
+
await page.fill(step.selector, step.value);
|
|
10351
|
+
} catch {
|
|
10352
|
+
const result = await healSelector(page, step.selector);
|
|
10353
|
+
if (result.found && result.locator) {
|
|
10354
|
+
await result.locator.fill(step.value);
|
|
10355
|
+
healed++;
|
|
10356
|
+
healedDetails.push({ step: i, original: step.selector, healed_to: `[healed:${result.method}]`, method: result.method });
|
|
10357
|
+
} else {
|
|
10358
|
+
throw new Error(`Type failed: ${step.selector} (self-healing exhausted)`);
|
|
10359
|
+
}
|
|
10360
|
+
}
|
|
10361
|
+
}
|
|
10362
|
+
break;
|
|
10363
|
+
case "scroll":
|
|
10364
|
+
if (step.y)
|
|
10365
|
+
await page.mouse.wheel(0, step.y);
|
|
10366
|
+
break;
|
|
10367
|
+
case "hover":
|
|
10368
|
+
if (step.selector) {
|
|
10369
|
+
try {
|
|
10370
|
+
await page.hover(step.selector);
|
|
10371
|
+
} catch {}
|
|
10372
|
+
}
|
|
10373
|
+
break;
|
|
10374
|
+
case "select":
|
|
10375
|
+
if (step.selector && step.value) {
|
|
10376
|
+
try {
|
|
10377
|
+
await page.selectOption(step.selector, step.value);
|
|
10378
|
+
} catch {}
|
|
10379
|
+
}
|
|
10380
|
+
break;
|
|
10381
|
+
case "wait":
|
|
10382
|
+
if (step.selector) {
|
|
10383
|
+
try {
|
|
10384
|
+
await page.waitForSelector(step.selector, { timeout: 1e4 });
|
|
10385
|
+
} catch {}
|
|
10386
|
+
} else {
|
|
10387
|
+
await new Promise((r) => setTimeout(r, step.timestamp || 1000));
|
|
10388
|
+
}
|
|
10389
|
+
break;
|
|
10390
|
+
case "evaluate":
|
|
10391
|
+
if (step.value)
|
|
10392
|
+
await page.evaluate(step.value);
|
|
10393
|
+
break;
|
|
10394
|
+
default:
|
|
10395
|
+
break;
|
|
10396
|
+
}
|
|
10397
|
+
executed++;
|
|
10398
|
+
} catch (err) {
|
|
10399
|
+
failed++;
|
|
10400
|
+
errors2.push(`Step ${i} (${step.type}): ${err instanceof Error ? err.message : String(err)}`);
|
|
10401
|
+
}
|
|
10402
|
+
}
|
|
10403
|
+
recordRun(workflow.id, healed > 0);
|
|
10404
|
+
return {
|
|
10405
|
+
success: failed === 0,
|
|
10406
|
+
steps_executed: executed,
|
|
10407
|
+
steps_failed: failed,
|
|
10408
|
+
steps_healed: healed,
|
|
10409
|
+
healed_details: healedDetails,
|
|
10410
|
+
errors: errors2,
|
|
10411
|
+
duration_ms: Date.now() - t0
|
|
10412
|
+
};
|
|
10413
|
+
}
|
|
10414
|
+
var init_workflows = __esm(() => {
|
|
10415
|
+
init_schema();
|
|
10416
|
+
});
|
|
10417
|
+
|
|
10418
|
+
// src/lib/auth-flow.ts
|
|
10419
|
+
var exports_auth_flow = {};
|
|
10420
|
+
__export(exports_auth_flow, {
|
|
10421
|
+
tryReplayAuth: () => tryReplayAuth,
|
|
10422
|
+
touchAuthFlow: () => touchAuthFlow,
|
|
10423
|
+
saveAuthFlow: () => saveAuthFlow,
|
|
10424
|
+
listAuthFlows: () => listAuthFlows,
|
|
10425
|
+
isAuthRedirect: () => isAuthRedirect,
|
|
10426
|
+
isAuthPage: () => isAuthPage,
|
|
10427
|
+
getAuthFlowByName: () => getAuthFlowByName,
|
|
10428
|
+
getAuthFlowByDomain: () => getAuthFlowByDomain,
|
|
10429
|
+
getAuthFlow: () => getAuthFlow,
|
|
10430
|
+
deleteAuthFlow: () => deleteAuthFlow
|
|
10431
|
+
});
|
|
10432
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
10433
|
+
function saveAuthFlow(data) {
|
|
10434
|
+
const db = getDatabase();
|
|
10435
|
+
const id = randomUUID11();
|
|
10436
|
+
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);
|
|
10437
|
+
return getAuthFlow(id);
|
|
10438
|
+
}
|
|
10439
|
+
function getAuthFlow(id) {
|
|
10440
|
+
const db = getDatabase();
|
|
10441
|
+
return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
|
|
10442
|
+
}
|
|
10443
|
+
function getAuthFlowByName(name) {
|
|
10444
|
+
const db = getDatabase();
|
|
10445
|
+
return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
|
|
10446
|
+
}
|
|
10447
|
+
function getAuthFlowByDomain(domain) {
|
|
10448
|
+
const db = getDatabase();
|
|
10449
|
+
return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
|
|
10450
|
+
}
|
|
10451
|
+
function listAuthFlows() {
|
|
10452
|
+
const db = getDatabase();
|
|
10453
|
+
return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
|
|
10454
|
+
}
|
|
10455
|
+
function deleteAuthFlow(name) {
|
|
10456
|
+
const db = getDatabase();
|
|
10457
|
+
const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
|
|
10458
|
+
return result.changes > 0;
|
|
10459
|
+
}
|
|
10460
|
+
function touchAuthFlow(id) {
|
|
10461
|
+
const db = getDatabase();
|
|
10462
|
+
db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
|
|
10463
|
+
}
|
|
10464
|
+
function isAuthPage(url) {
|
|
10465
|
+
return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
10466
|
+
}
|
|
10467
|
+
function isAuthRedirect(fromUrl, toUrl) {
|
|
10468
|
+
if (fromUrl === toUrl)
|
|
10469
|
+
return false;
|
|
10470
|
+
return isAuthPage(toUrl) && !isAuthPage(fromUrl);
|
|
10471
|
+
}
|
|
10472
|
+
async function tryReplayAuth(page, domain) {
|
|
10473
|
+
const flow = getAuthFlowByDomain(domain);
|
|
10474
|
+
if (!flow)
|
|
10475
|
+
return { replayed: false };
|
|
10476
|
+
if (flow.storage_state_path) {
|
|
10477
|
+
try {
|
|
10478
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
|
|
10479
|
+
if (existsSync5(flow.storage_state_path)) {
|
|
10480
|
+
const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
|
|
10481
|
+
if (state.cookies?.length) {
|
|
10482
|
+
await page.context().addCookies(state.cookies);
|
|
10483
|
+
await page.reload();
|
|
10484
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
10485
|
+
if (!isAuthPage(page.url())) {
|
|
10486
|
+
touchAuthFlow(flow.id);
|
|
10487
|
+
return { replayed: true, flow, method: "storage_state" };
|
|
10488
|
+
}
|
|
10489
|
+
}
|
|
10490
|
+
}
|
|
10491
|
+
} catch {}
|
|
10492
|
+
}
|
|
10493
|
+
if (flow.recording_id) {
|
|
10494
|
+
try {
|
|
10495
|
+
const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
10496
|
+
const result = await replayRecording2(flow.recording_id, page);
|
|
10497
|
+
if (result.success) {
|
|
10498
|
+
try {
|
|
10499
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
10500
|
+
const path = await saveStateFromPage2(page, flow.name);
|
|
10501
|
+
const db = getDatabase();
|
|
10502
|
+
db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
|
|
10503
|
+
} catch {}
|
|
10504
|
+
touchAuthFlow(flow.id);
|
|
10505
|
+
return { replayed: true, flow, method: "recording_replay" };
|
|
10506
|
+
}
|
|
10507
|
+
} catch {}
|
|
10508
|
+
}
|
|
10509
|
+
return { replayed: false, flow };
|
|
10510
|
+
}
|
|
10511
|
+
var AUTH_URL_PATTERNS;
|
|
10512
|
+
var init_auth_flow = __esm(() => {
|
|
10513
|
+
init_schema();
|
|
10514
|
+
AUTH_URL_PATTERNS = [
|
|
10515
|
+
/\/login/i,
|
|
10516
|
+
/\/signin/i,
|
|
10517
|
+
/\/sign-in/i,
|
|
10518
|
+
/\/auth/i,
|
|
10519
|
+
/\/sso/i,
|
|
10520
|
+
/\/oauth/i,
|
|
10521
|
+
/\/cas\/login/i,
|
|
10522
|
+
/accounts\.google\.com/i,
|
|
10523
|
+
/login\.microsoftonline\.com/i,
|
|
10524
|
+
/github\.com\/login/i,
|
|
10525
|
+
/auth0\.com/i
|
|
10526
|
+
];
|
|
10527
|
+
});
|
|
10528
|
+
|
|
10529
|
+
// src/lib/vision-fallback.ts
|
|
10530
|
+
var exports_vision_fallback = {};
|
|
10531
|
+
__export(exports_vision_fallback, {
|
|
10532
|
+
findElementByVision: () => findElementByVision,
|
|
10533
|
+
clickByVision: () => clickByVision
|
|
10534
|
+
});
|
|
10535
|
+
async function findElementByVision(page, description, opts) {
|
|
10536
|
+
const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
|
|
10537
|
+
const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
10538
|
+
const base64 = screenshot.toString("base64");
|
|
10539
|
+
const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
|
|
10540
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
10541
|
+
if (!apiKey) {
|
|
10542
|
+
return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
|
|
10543
|
+
}
|
|
10544
|
+
try {
|
|
10545
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
10546
|
+
method: "POST",
|
|
10547
|
+
headers: {
|
|
10548
|
+
"content-type": "application/json",
|
|
10549
|
+
"x-api-key": apiKey,
|
|
10550
|
+
"anthropic-version": "2023-06-01"
|
|
10551
|
+
},
|
|
10552
|
+
body: JSON.stringify({
|
|
10553
|
+
model,
|
|
10554
|
+
max_tokens: 256,
|
|
10555
|
+
messages: [{
|
|
10556
|
+
role: "user",
|
|
10557
|
+
content: [
|
|
10558
|
+
{
|
|
10559
|
+
type: "image",
|
|
10560
|
+
source: { type: "base64", media_type: "image/jpeg", data: base64 }
|
|
10561
|
+
},
|
|
10562
|
+
{
|
|
10563
|
+
type: "text",
|
|
10564
|
+
text: `Find the element matching this description: "${description}"
|
|
10565
|
+
|
|
10566
|
+
The screenshot is ${viewport.width}x${viewport.height} pixels.
|
|
10567
|
+
|
|
10568
|
+
Reply with ONLY a JSON object (no markdown, no explanation):
|
|
10569
|
+
{"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
|
|
10570
|
+
|
|
10571
|
+
If you cannot find the element:
|
|
10572
|
+
{"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
|
|
10573
|
+
}
|
|
10574
|
+
]
|
|
10575
|
+
}]
|
|
10576
|
+
})
|
|
10577
|
+
});
|
|
10578
|
+
const data = await response.json();
|
|
10579
|
+
const text = data.content?.[0]?.text ?? "";
|
|
10580
|
+
const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
|
|
10581
|
+
const result = JSON.parse(jsonStr);
|
|
10582
|
+
result.model = model;
|
|
10583
|
+
return result;
|
|
10584
|
+
} catch (err) {
|
|
10585
|
+
return {
|
|
10586
|
+
found: false,
|
|
10587
|
+
x: 0,
|
|
10588
|
+
y: 0,
|
|
10589
|
+
confidence: "none",
|
|
10590
|
+
description,
|
|
10591
|
+
model,
|
|
10592
|
+
error: err instanceof Error ? err.message : String(err)
|
|
10593
|
+
};
|
|
10594
|
+
}
|
|
10595
|
+
}
|
|
10596
|
+
async function clickByVision(page, description, opts) {
|
|
10597
|
+
const result = await findElementByVision(page, description, opts);
|
|
10598
|
+
if (result.found && result.x > 0 && result.y > 0) {
|
|
10599
|
+
await page.mouse.click(result.x, result.y);
|
|
10600
|
+
}
|
|
10601
|
+
return result;
|
|
10602
|
+
}
|
|
10603
|
+
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
10604
|
+
|
|
10605
|
+
// src/lib/api-detector.ts
|
|
10606
|
+
var exports_api_detector = {};
|
|
10607
|
+
__export(exports_api_detector, {
|
|
10608
|
+
listDiscoveredAPIs: () => listDiscoveredAPIs,
|
|
10609
|
+
detectAPIs: () => detectAPIs
|
|
10610
|
+
});
|
|
10611
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
10612
|
+
function detectAPIs(sessionId) {
|
|
10613
|
+
const db = getDatabase();
|
|
10614
|
+
const requests = db.query(`SELECT method, url, status_code, response_headers, body_size
|
|
10615
|
+
FROM network_log
|
|
10616
|
+
WHERE session_id = ?
|
|
10617
|
+
AND (response_headers LIKE '%application/json%' OR response_headers LIKE '%text/json%')
|
|
10618
|
+
AND status_code >= 200 AND status_code < 400
|
|
10619
|
+
ORDER BY timestamp DESC`).all(sessionId);
|
|
10620
|
+
const seen = new Map;
|
|
10621
|
+
for (const req of requests) {
|
|
10622
|
+
try {
|
|
10623
|
+
const urlObj = new URL(req.url);
|
|
10624
|
+
if (urlObj.pathname.match(/\.(js|css|png|jpg|svg|woff|ico)$/))
|
|
10625
|
+
continue;
|
|
10626
|
+
if (urlObj.hostname.includes("googleapis.com/identitytoolkit"))
|
|
10627
|
+
continue;
|
|
10628
|
+
if (urlObj.hostname.includes("posthog"))
|
|
10629
|
+
continue;
|
|
10630
|
+
if (urlObj.hostname.includes("sentry"))
|
|
10631
|
+
continue;
|
|
10632
|
+
const key = `${req.method} ${urlObj.origin}${urlObj.pathname}`;
|
|
10633
|
+
if (!seen.has(key)) {
|
|
10634
|
+
seen.set(key, {
|
|
10635
|
+
url: `${urlObj.origin}${urlObj.pathname}`,
|
|
10636
|
+
method: req.method,
|
|
10637
|
+
status_code: req.status_code,
|
|
10638
|
+
content_type: "application/json",
|
|
10639
|
+
response_schema: {},
|
|
10640
|
+
sample_size: req.body_size ?? 0
|
|
10641
|
+
});
|
|
10642
|
+
}
|
|
10643
|
+
} catch {}
|
|
10644
|
+
}
|
|
10645
|
+
const apis = Array.from(seen.values());
|
|
10646
|
+
for (const api of apis) {
|
|
10647
|
+
const id = randomUUID12();
|
|
10648
|
+
db.prepare("INSERT OR IGNORE INTO api_endpoints (id, session_id, url, method, status_code, content_type) VALUES (?, ?, ?, ?, ?, ?)").run(id, sessionId, api.url, api.method, api.status_code, api.content_type);
|
|
10649
|
+
}
|
|
10650
|
+
return apis;
|
|
10651
|
+
}
|
|
10652
|
+
function listDiscoveredAPIs(sessionId) {
|
|
10653
|
+
const db = getDatabase();
|
|
10654
|
+
if (sessionId) {
|
|
10655
|
+
return db.query("SELECT * FROM api_endpoints WHERE session_id = ? ORDER BY discovered_at DESC").all(sessionId);
|
|
10656
|
+
}
|
|
10657
|
+
return db.query("SELECT * FROM api_endpoints ORDER BY discovered_at DESC LIMIT 100").all();
|
|
10658
|
+
}
|
|
10659
|
+
var init_api_detector = __esm(() => {
|
|
10660
|
+
init_schema();
|
|
10661
|
+
});
|
|
10662
|
+
|
|
10663
|
+
// src/lib/structured-extract.ts
|
|
10664
|
+
var exports_structured_extract = {};
|
|
10665
|
+
__export(exports_structured_extract, {
|
|
10666
|
+
extractStructuredData: () => extractStructuredData
|
|
10667
|
+
});
|
|
10668
|
+
async function extractStructuredData(page) {
|
|
10669
|
+
return page.evaluate(() => {
|
|
10670
|
+
const result = { tables: [], lists: [], jsonLd: [], openGraph: {}, metaTags: {}, repeatedElements: [] };
|
|
10671
|
+
document.querySelectorAll("table").forEach((table, idx) => {
|
|
10672
|
+
const headers = [];
|
|
10673
|
+
table.querySelectorAll("thead th, thead td, tr:first-child th").forEach((th) => {
|
|
10674
|
+
headers.push(th.textContent?.trim() ?? "");
|
|
10675
|
+
});
|
|
10676
|
+
const rows = [];
|
|
10677
|
+
table.querySelectorAll("tbody tr, tr:not(:first-child)").forEach((tr) => {
|
|
10678
|
+
const row = [];
|
|
10679
|
+
tr.querySelectorAll("td, th").forEach((td) => {
|
|
10680
|
+
row.push(td.textContent?.trim() ?? "");
|
|
10681
|
+
});
|
|
10682
|
+
if (row.length > 0 && row.some((c) => c !== ""))
|
|
10683
|
+
rows.push(row);
|
|
10684
|
+
});
|
|
10685
|
+
if (rows.length > 0) {
|
|
10686
|
+
result.tables.push({ headers, rows, selector: `table:nth-of-type(${idx + 1})` });
|
|
10687
|
+
}
|
|
10688
|
+
});
|
|
10689
|
+
document.querySelectorAll("ul, ol").forEach((list, idx) => {
|
|
10690
|
+
const items = [];
|
|
10691
|
+
list.querySelectorAll(":scope > li").forEach((li) => {
|
|
10692
|
+
const text = li.textContent?.trim() ?? "";
|
|
10693
|
+
if (text)
|
|
10694
|
+
items.push(text);
|
|
10695
|
+
});
|
|
10696
|
+
if (items.length >= 3) {
|
|
10697
|
+
const tag = list.tagName.toLowerCase();
|
|
10698
|
+
result.lists.push({ items, selector: `${tag}:nth-of-type(${idx + 1})` });
|
|
10699
|
+
}
|
|
10700
|
+
});
|
|
10701
|
+
document.querySelectorAll('script[type="application/ld+json"]').forEach((script) => {
|
|
10702
|
+
try {
|
|
10703
|
+
result.jsonLd.push(JSON.parse(script.textContent ?? ""));
|
|
10704
|
+
} catch {}
|
|
10705
|
+
});
|
|
10706
|
+
document.querySelectorAll('meta[property^="og:"]').forEach((meta) => {
|
|
10707
|
+
const prop = meta.getAttribute("property")?.replace("og:", "") ?? "";
|
|
10708
|
+
result.openGraph[prop] = meta.getAttribute("content") ?? "";
|
|
10709
|
+
});
|
|
10710
|
+
document.querySelectorAll("meta[name]").forEach((meta) => {
|
|
10711
|
+
const name = meta.getAttribute("name") ?? "";
|
|
10712
|
+
if (name)
|
|
10713
|
+
result.metaTags[name] = meta.getAttribute("content") ?? "";
|
|
10714
|
+
});
|
|
10715
|
+
const classCounts = new Map;
|
|
10716
|
+
document.querySelectorAll("[class]").forEach((el) => {
|
|
10717
|
+
const cls = el.className.toString().trim();
|
|
10718
|
+
if (cls && cls.length > 5 && cls.length < 100) {
|
|
10719
|
+
if (!classCounts.has(cls))
|
|
10720
|
+
classCounts.set(cls, []);
|
|
10721
|
+
classCounts.get(cls).push(el);
|
|
10722
|
+
}
|
|
10723
|
+
});
|
|
10724
|
+
for (const [cls, elements] of classCounts) {
|
|
10725
|
+
if (elements.length >= 3 && elements.length <= 200) {
|
|
10726
|
+
const sample = elements.slice(0, 3).map((el) => el.textContent?.trim().slice(0, 100) ?? "");
|
|
10727
|
+
if (sample.some((s) => s.length > 10)) {
|
|
10728
|
+
result.repeatedElements.push({
|
|
10729
|
+
selector: `.${cls.split(" ")[0]}`,
|
|
10730
|
+
count: elements.length,
|
|
10731
|
+
sample
|
|
10732
|
+
});
|
|
10733
|
+
}
|
|
10734
|
+
}
|
|
10735
|
+
}
|
|
10736
|
+
result.repeatedElements.sort((a, b) => b.count - a.count);
|
|
10737
|
+
result.repeatedElements = result.repeatedElements.slice(0, 10);
|
|
10738
|
+
return result;
|
|
10739
|
+
});
|
|
10740
|
+
}
|
|
10741
|
+
|
|
10742
|
+
// src/lib/datasets.ts
|
|
10743
|
+
var exports_datasets = {};
|
|
10744
|
+
__export(exports_datasets, {
|
|
10745
|
+
saveDataset: () => saveDataset,
|
|
10746
|
+
listDatasets: () => listDatasets,
|
|
10747
|
+
getDatasetByName: () => getDatasetByName,
|
|
10748
|
+
getDataset: () => getDataset,
|
|
10749
|
+
exportDataset: () => exportDataset,
|
|
10750
|
+
deleteDataset: () => deleteDataset
|
|
10751
|
+
});
|
|
10752
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
10753
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync9 } from "fs";
|
|
10754
|
+
import { join as join9 } from "path";
|
|
10755
|
+
import { homedir as homedir9 } from "os";
|
|
10756
|
+
function saveDataset(data) {
|
|
10757
|
+
const db = getDatabase();
|
|
10758
|
+
const id = randomUUID13();
|
|
10759
|
+
const existing = db.query("SELECT id FROM datasets WHERE name = ?").get(data.name);
|
|
10760
|
+
if (existing) {
|
|
10761
|
+
db.prepare("UPDATE datasets SET data = ?, row_count = ?, source_url = ?, schema = ?, last_refresh = datetime('now'), updated_at = datetime('now') WHERE name = ?").run(JSON.stringify(data.rows), data.rows.length, data.sourceUrl ?? null, data.schema ? JSON.stringify(data.schema) : null, data.name);
|
|
10762
|
+
return getDataset(existing.id);
|
|
10763
|
+
}
|
|
10764
|
+
db.prepare("INSERT INTO datasets (id, name, source_url, source_type, data, row_count, schema) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.name, data.sourceUrl ?? null, data.sourceType ?? "page", JSON.stringify(data.rows), data.rows.length, data.schema ? JSON.stringify(data.schema) : null);
|
|
10765
|
+
return getDataset(id);
|
|
10766
|
+
}
|
|
10767
|
+
function getDataset(id) {
|
|
10768
|
+
const db = getDatabase();
|
|
10769
|
+
const row = db.query("SELECT * FROM datasets WHERE id = ?").get(id);
|
|
10770
|
+
if (!row)
|
|
10771
|
+
return null;
|
|
10772
|
+
return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
|
|
10773
|
+
}
|
|
10774
|
+
function getDatasetByName(name) {
|
|
10775
|
+
const db = getDatabase();
|
|
10776
|
+
const row = db.query("SELECT * FROM datasets WHERE name = ?").get(name);
|
|
10777
|
+
if (!row)
|
|
10778
|
+
return null;
|
|
10779
|
+
return { ...row, data: JSON.parse(row.data), schema: row.schema ? JSON.parse(row.schema) : null };
|
|
10780
|
+
}
|
|
10781
|
+
function listDatasets() {
|
|
10782
|
+
const db = getDatabase();
|
|
10783
|
+
return db.query("SELECT id, name, source_url, source_type, row_count, last_refresh, created_at, updated_at FROM datasets ORDER BY updated_at DESC").all().map((row) => ({ ...row, data: `${row.row_count} rows`, schema: null }));
|
|
10784
|
+
}
|
|
10785
|
+
function deleteDataset(name) {
|
|
10786
|
+
const db = getDatabase();
|
|
10787
|
+
return db.prepare("DELETE FROM datasets WHERE name = ?").run(name).changes > 0;
|
|
10788
|
+
}
|
|
10789
|
+
function exportDataset(name, format) {
|
|
10790
|
+
const dataset = getDatasetByName(name);
|
|
10791
|
+
if (!dataset)
|
|
10792
|
+
throw new Error(`Dataset '${name}' not found`);
|
|
10793
|
+
const dir = join9(process.env["BROWSER_DATA_DIR"] ?? join9(homedir9(), ".browser"), "exports");
|
|
10794
|
+
mkdirSync9(dir, { recursive: true });
|
|
10795
|
+
const filename = `${name}.${format}`;
|
|
10796
|
+
const path = join9(dir, filename);
|
|
10797
|
+
if (format === "csv") {
|
|
10798
|
+
const rows = dataset.data;
|
|
10799
|
+
if (rows.length === 0) {
|
|
10800
|
+
writeFileSync3(path, "");
|
|
10801
|
+
return { path, size: 0 };
|
|
10802
|
+
}
|
|
10803
|
+
const headers = Object.keys(rows[0]);
|
|
10804
|
+
const csvLines = [headers.join(",")];
|
|
10805
|
+
for (const row of rows) {
|
|
10806
|
+
csvLines.push(headers.map((h) => {
|
|
10807
|
+
const val = String(row[h] ?? "");
|
|
10808
|
+
return val.includes(",") || val.includes('"') ? `"${val.replace(/"/g, '""')}"` : val;
|
|
10809
|
+
}).join(","));
|
|
10810
|
+
}
|
|
10811
|
+
const content = csvLines.join(`
|
|
10812
|
+
`);
|
|
10813
|
+
writeFileSync3(path, content);
|
|
10814
|
+
return { path, size: content.length };
|
|
10815
|
+
} else {
|
|
10816
|
+
const content = JSON.stringify(dataset.data, null, 2);
|
|
10817
|
+
writeFileSync3(path, content);
|
|
10818
|
+
return { path, size: content.length };
|
|
10819
|
+
}
|
|
10820
|
+
}
|
|
10821
|
+
var init_datasets = __esm(() => {
|
|
10822
|
+
init_schema();
|
|
10823
|
+
});
|
|
10824
|
+
|
|
9264
10825
|
// src/lib/auth.ts
|
|
9265
10826
|
var exports_auth = {};
|
|
9266
10827
|
__export(exports_auth, {
|
|
9267
10828
|
loginWithCredentials: () => loginWithCredentials,
|
|
9268
10829
|
getCredentials: () => getCredentials
|
|
9269
10830
|
});
|
|
9270
|
-
import { existsSync as
|
|
9271
|
-
import { join as
|
|
9272
|
-
import { homedir as
|
|
10831
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
10832
|
+
import { join as join10 } from "path";
|
|
10833
|
+
import { homedir as homedir10 } from "os";
|
|
9273
10834
|
async function getCredentials(service) {
|
|
9274
10835
|
try {
|
|
9275
|
-
const { getSecret } = await import(`${
|
|
10836
|
+
const { getSecret } = await import(`${homedir10()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
|
|
9276
10837
|
const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
|
|
9277
10838
|
const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
|
|
9278
10839
|
if (email?.value && password?.value) {
|
|
9279
10840
|
return { email: email.value, password: password.value };
|
|
9280
10841
|
}
|
|
9281
10842
|
} catch {}
|
|
9282
|
-
const secretsPath =
|
|
9283
|
-
if (
|
|
10843
|
+
const secretsPath = join10(homedir10(), ".secrets");
|
|
10844
|
+
if (existsSync5(secretsPath)) {
|
|
9284
10845
|
const content = readFileSync3(secretsPath, "utf8");
|
|
9285
10846
|
const lines = content.split(`
|
|
9286
10847
|
`);
|
|
@@ -9457,14 +11018,14 @@ __export(exports_dist, {
|
|
|
9457
11018
|
InvalidScopeError: () => InvalidScopeError,
|
|
9458
11019
|
EntityNotFoundError: () => EntityNotFoundError,
|
|
9459
11020
|
DuplicateMemoryError: () => DuplicateMemoryError,
|
|
9460
|
-
DEFAULT_MODEL: () =>
|
|
11021
|
+
DEFAULT_MODEL: () => DEFAULT_MODEL2,
|
|
9461
11022
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG
|
|
9462
11023
|
});
|
|
9463
11024
|
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
|
|
11025
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync10 } from "fs";
|
|
11026
|
+
import { dirname, join as join11, resolve } from "path";
|
|
11027
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync4, unlinkSync as unlinkSync3 } from "fs";
|
|
11028
|
+
import { homedir as homedir11 } from "os";
|
|
9468
11029
|
import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
|
|
9469
11030
|
import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
|
|
9470
11031
|
import { homedir as homedir22 } from "os";
|
|
@@ -9478,8 +11039,8 @@ function isInMemoryDb(path) {
|
|
|
9478
11039
|
function findNearestMementosDb(startDir) {
|
|
9479
11040
|
let dir = resolve(startDir);
|
|
9480
11041
|
while (true) {
|
|
9481
|
-
const candidate =
|
|
9482
|
-
if (
|
|
11042
|
+
const candidate = join11(dir, ".mementos", "mementos.db");
|
|
11043
|
+
if (existsSync6(candidate))
|
|
9483
11044
|
return candidate;
|
|
9484
11045
|
const parent = dirname(dir);
|
|
9485
11046
|
if (parent === dir)
|
|
@@ -9491,7 +11052,7 @@ function findNearestMementosDb(startDir) {
|
|
|
9491
11052
|
function findGitRoot(startDir) {
|
|
9492
11053
|
let dir = resolve(startDir);
|
|
9493
11054
|
while (true) {
|
|
9494
|
-
if (
|
|
11055
|
+
if (existsSync6(join11(dir, ".git")))
|
|
9495
11056
|
return dir;
|
|
9496
11057
|
const parent = dirname(dir);
|
|
9497
11058
|
if (parent === dir)
|
|
@@ -9511,25 +11072,25 @@ function getDbPath() {
|
|
|
9511
11072
|
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
9512
11073
|
const gitRoot = findGitRoot(cwd);
|
|
9513
11074
|
if (gitRoot) {
|
|
9514
|
-
return
|
|
11075
|
+
return join11(gitRoot, ".mementos", "mementos.db");
|
|
9515
11076
|
}
|
|
9516
11077
|
}
|
|
9517
11078
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
9518
|
-
return
|
|
11079
|
+
return join11(home, ".mementos", "mementos.db");
|
|
9519
11080
|
}
|
|
9520
|
-
function
|
|
11081
|
+
function ensureDir2(filePath) {
|
|
9521
11082
|
if (isInMemoryDb(filePath))
|
|
9522
11083
|
return;
|
|
9523
11084
|
const dir = dirname(resolve(filePath));
|
|
9524
|
-
if (!
|
|
9525
|
-
|
|
11085
|
+
if (!existsSync6(dir)) {
|
|
11086
|
+
mkdirSync10(dir, { recursive: true });
|
|
9526
11087
|
}
|
|
9527
11088
|
}
|
|
9528
11089
|
function getDatabase2(dbPath) {
|
|
9529
11090
|
if (_db2)
|
|
9530
11091
|
return _db2;
|
|
9531
11092
|
const path = dbPath || getDbPath();
|
|
9532
|
-
|
|
11093
|
+
ensureDir2(path);
|
|
9533
11094
|
_db2 = new Database2(path, { create: true });
|
|
9534
11095
|
_db2.run("PRAGMA journal_mode = WAL");
|
|
9535
11096
|
_db2.run("PRAGMA busy_timeout = 5000");
|
|
@@ -11372,7 +12933,7 @@ function isValidCategory(value) {
|
|
|
11372
12933
|
return VALID_CATEGORIES.includes(value);
|
|
11373
12934
|
}
|
|
11374
12935
|
function loadConfig() {
|
|
11375
|
-
const configPath = join22(
|
|
12936
|
+
const configPath = join22(homedir11(), ".mementos", "config.json");
|
|
11376
12937
|
let fileConfig = {};
|
|
11377
12938
|
if (existsSync22(configPath)) {
|
|
11378
12939
|
try {
|
|
@@ -11399,10 +12960,10 @@ function loadConfig() {
|
|
|
11399
12960
|
return merged;
|
|
11400
12961
|
}
|
|
11401
12962
|
function profilesDir() {
|
|
11402
|
-
return join22(
|
|
12963
|
+
return join22(homedir11(), ".mementos", "profiles");
|
|
11403
12964
|
}
|
|
11404
12965
|
function globalConfigPath() {
|
|
11405
|
-
return join22(
|
|
12966
|
+
return join22(homedir11(), ".mementos", "config.json");
|
|
11406
12967
|
}
|
|
11407
12968
|
function readGlobalConfig() {
|
|
11408
12969
|
const p = globalConfigPath();
|
|
@@ -11416,8 +12977,8 @@ function readGlobalConfig() {
|
|
|
11416
12977
|
}
|
|
11417
12978
|
function writeGlobalConfig(data) {
|
|
11418
12979
|
const p = globalConfigPath();
|
|
11419
|
-
|
|
11420
|
-
|
|
12980
|
+
ensureDir22(dirname2(p));
|
|
12981
|
+
writeFileSync4(p, JSON.stringify(data, null, 2), "utf-8");
|
|
11421
12982
|
}
|
|
11422
12983
|
function getActiveProfile() {
|
|
11423
12984
|
const envProfile = process.env["MEMENTOS_PROFILE"];
|
|
@@ -11439,18 +13000,18 @@ function listProfiles2() {
|
|
|
11439
13000
|
const dir = profilesDir();
|
|
11440
13001
|
if (!existsSync22(dir))
|
|
11441
13002
|
return [];
|
|
11442
|
-
return
|
|
13003
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
|
|
11443
13004
|
}
|
|
11444
13005
|
function deleteProfile2(name) {
|
|
11445
13006
|
const dbPath = join22(profilesDir(), `${name}.db`);
|
|
11446
13007
|
if (!existsSync22(dbPath))
|
|
11447
13008
|
return false;
|
|
11448
|
-
|
|
13009
|
+
unlinkSync3(dbPath);
|
|
11449
13010
|
if (getActiveProfile() === name)
|
|
11450
13011
|
setActiveProfile(null);
|
|
11451
13012
|
return true;
|
|
11452
13013
|
}
|
|
11453
|
-
function
|
|
13014
|
+
function ensureDir22(dir) {
|
|
11454
13015
|
if (!existsSync22(dir)) {
|
|
11455
13016
|
mkdirSync22(dir, { recursive: true });
|
|
11456
13017
|
}
|
|
@@ -12572,7 +14133,7 @@ function writeConfig(config) {
|
|
|
12572
14133
|
}
|
|
12573
14134
|
function getActiveModel() {
|
|
12574
14135
|
const config = readConfig();
|
|
12575
|
-
return config.activeModel ??
|
|
14136
|
+
return config.activeModel ?? DEFAULT_MODEL2;
|
|
12576
14137
|
}
|
|
12577
14138
|
function setActiveModel(modelId) {
|
|
12578
14139
|
const config = readConfig();
|
|
@@ -12646,7 +14207,7 @@ Return JSON with this exact shape:
|
|
|
12646
14207
|
examples: finalExamples,
|
|
12647
14208
|
count: finalExamples.length
|
|
12648
14209
|
};
|
|
12649
|
-
},
|
|
14210
|
+
}, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
|
|
12650
14211
|
var init_dist = __esm(() => {
|
|
12651
14212
|
__defProp2 = Object.defineProperty;
|
|
12652
14213
|
exports_database = {};
|
|
@@ -14082,10 +15643,10 @@ __export(exports_dist2, {
|
|
|
14082
15643
|
acquireLock: () => acquireLock2
|
|
14083
15644
|
});
|
|
14084
15645
|
import { Database as Database3 } from "bun:sqlite";
|
|
14085
|
-
import { mkdirSync as
|
|
14086
|
-
import { join as
|
|
14087
|
-
import { homedir as
|
|
14088
|
-
import { randomUUID as
|
|
15646
|
+
import { mkdirSync as mkdirSync11 } from "fs";
|
|
15647
|
+
import { join as join12, dirname as dirname3 } from "path";
|
|
15648
|
+
import { homedir as homedir12 } from "os";
|
|
15649
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
14089
15650
|
import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
|
|
14090
15651
|
import { join as join33 } from "path";
|
|
14091
15652
|
import { homedir as homedir33 } from "os";
|
|
@@ -14093,19 +15654,19 @@ import { readFileSync as readFileSync5 } from "fs";
|
|
|
14093
15654
|
import { join as join23 } from "path";
|
|
14094
15655
|
import { homedir as homedir23 } from "os";
|
|
14095
15656
|
import { randomUUID as randomUUID22 } from "crypto";
|
|
14096
|
-
import { readFileSync as readFileSync23, writeFileSync as
|
|
15657
|
+
import { readFileSync as readFileSync23, writeFileSync as writeFileSync5, mkdirSync as mkdirSync33 } from "fs";
|
|
14097
15658
|
import { join as join43, dirname as dirname22 } from "path";
|
|
14098
15659
|
import { homedir as homedir42 } from "os";
|
|
14099
15660
|
function getDbPath2() {
|
|
14100
15661
|
if (process.env.CONVERSATIONS_DB_PATH)
|
|
14101
15662
|
return process.env.CONVERSATIONS_DB_PATH;
|
|
14102
|
-
return
|
|
15663
|
+
return join12(homedir12(), ".conversations", "messages.db");
|
|
14103
15664
|
}
|
|
14104
15665
|
function getDb() {
|
|
14105
15666
|
if (db)
|
|
14106
15667
|
return db;
|
|
14107
15668
|
const dbPath = getDbPath2();
|
|
14108
|
-
|
|
15669
|
+
mkdirSync11(dirname3(dbPath), { recursive: true });
|
|
14109
15670
|
db = new Database3(dbPath, { create: true });
|
|
14110
15671
|
db.exec("PRAGMA journal_mode = WAL");
|
|
14111
15672
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -14465,7 +16026,7 @@ function guessMimeType(name) {
|
|
|
14465
16026
|
function sendMessage(opts) {
|
|
14466
16027
|
const db2 = getDb();
|
|
14467
16028
|
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("-")}-${
|
|
16029
|
+
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID14().slice(0, 8)}`);
|
|
14469
16030
|
const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
|
|
14470
16031
|
const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
|
|
14471
16032
|
const blocking = opts.blocking ? 1 : 0;
|
|
@@ -15293,7 +16854,7 @@ function getAutoName() {
|
|
|
15293
16854
|
cachedAutoName = name;
|
|
15294
16855
|
try {
|
|
15295
16856
|
mkdirSync33(dirname22(AGENT_ID_FILE), { recursive: true });
|
|
15296
|
-
|
|
16857
|
+
writeFileSync5(AGENT_ID_FILE, name + `
|
|
15297
16858
|
`, "utf-8");
|
|
15298
16859
|
} catch {}
|
|
15299
16860
|
return name;
|
|
@@ -17248,7 +18809,7 @@ Your code should look like:
|
|
|
17248
18809
|
}
|
|
17249
18810
|
}
|
|
17250
18811
|
}
|
|
17251
|
-
function checkPropTypes(typeSpecs, values,
|
|
18812
|
+
function checkPropTypes(typeSpecs, values, location2, componentName, element) {
|
|
17252
18813
|
{
|
|
17253
18814
|
var has = Function.call.bind(hasOwnProperty);
|
|
17254
18815
|
for (var typeSpecName in typeSpecs) {
|
|
@@ -17256,23 +18817,23 @@ Your code should look like:
|
|
|
17256
18817
|
var error$1 = undefined;
|
|
17257
18818
|
try {
|
|
17258
18819
|
if (typeof typeSpecs[typeSpecName] !== "function") {
|
|
17259
|
-
var err = Error((componentName || "React class") + ": " +
|
|
18820
|
+
var err = Error((componentName || "React class") + ": " + location2 + " type `" + typeSpecName + "` is invalid; " + "it must be a function, usually from the `prop-types` package, but received `" + typeof typeSpecs[typeSpecName] + "`." + "This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.");
|
|
17260
18821
|
err.name = "Invariant Violation";
|
|
17261
18822
|
throw err;
|
|
17262
18823
|
}
|
|
17263
|
-
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName,
|
|
18824
|
+
error$1 = typeSpecs[typeSpecName](values, typeSpecName, componentName, location2, null, "SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED");
|
|
17264
18825
|
} catch (ex) {
|
|
17265
18826
|
error$1 = ex;
|
|
17266
18827
|
}
|
|
17267
18828
|
if (error$1 && !(error$1 instanceof Error)) {
|
|
17268
18829
|
setCurrentlyValidatingElement(element);
|
|
17269
|
-
error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class",
|
|
18830
|
+
error("%s: type specification of %s" + " `%s` is invalid; the type checker " + "function must return `null` or an `Error` but returned a %s. " + "You may have forgotten to pass an argument to the type checker " + "creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and " + "shape all require an argument).", componentName || "React class", location2, typeSpecName, typeof error$1);
|
|
17270
18831
|
setCurrentlyValidatingElement(null);
|
|
17271
18832
|
}
|
|
17272
18833
|
if (error$1 instanceof Error && !(error$1.message in loggedTypeFailures)) {
|
|
17273
18834
|
loggedTypeFailures[error$1.message] = true;
|
|
17274
18835
|
setCurrentlyValidatingElement(element);
|
|
17275
|
-
error("Failed %s type: %s",
|
|
18836
|
+
error("Failed %s type: %s", location2, error$1.message);
|
|
17276
18837
|
setCurrentlyValidatingElement(null);
|
|
17277
18838
|
}
|
|
17278
18839
|
}
|
|
@@ -18507,11 +20068,11 @@ __export(exports_dist3, {
|
|
|
18507
20068
|
AgentNotFoundError: () => AgentNotFoundError2
|
|
18508
20069
|
});
|
|
18509
20070
|
import { Database as Database4 } from "bun:sqlite";
|
|
18510
|
-
import { existsSync as
|
|
18511
|
-
import { dirname as dirname5, join as
|
|
20071
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync12 } from "fs";
|
|
20072
|
+
import { dirname as dirname5, join as join13, resolve as resolve3 } from "path";
|
|
18512
20073
|
import { existsSync as existsSync33 } from "fs";
|
|
18513
20074
|
import { join as join34 } from "path";
|
|
18514
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as
|
|
20075
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "fs";
|
|
18515
20076
|
import { join as join24 } from "path";
|
|
18516
20077
|
import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
|
|
18517
20078
|
import { join as join44 } from "path";
|
|
@@ -18741,8 +20302,8 @@ function isInMemoryDb2(path) {
|
|
|
18741
20302
|
function findNearestTodosDb(startDir) {
|
|
18742
20303
|
let dir = resolve3(startDir);
|
|
18743
20304
|
while (true) {
|
|
18744
|
-
const candidate =
|
|
18745
|
-
if (
|
|
20305
|
+
const candidate = join13(dir, ".todos", "todos.db");
|
|
20306
|
+
if (existsSync7(candidate))
|
|
18746
20307
|
return candidate;
|
|
18747
20308
|
const parent = dirname5(dir);
|
|
18748
20309
|
if (parent === dir)
|
|
@@ -18754,7 +20315,7 @@ function findNearestTodosDb(startDir) {
|
|
|
18754
20315
|
function findGitRoot2(startDir) {
|
|
18755
20316
|
let dir = resolve3(startDir);
|
|
18756
20317
|
while (true) {
|
|
18757
|
-
if (
|
|
20318
|
+
if (existsSync7(join13(dir, ".git")))
|
|
18758
20319
|
return dir;
|
|
18759
20320
|
const parent = dirname5(dir);
|
|
18760
20321
|
if (parent === dir)
|
|
@@ -18774,18 +20335,18 @@ function getDbPath3() {
|
|
|
18774
20335
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
18775
20336
|
const gitRoot = findGitRoot2(cwd);
|
|
18776
20337
|
if (gitRoot) {
|
|
18777
|
-
return
|
|
20338
|
+
return join13(gitRoot, ".todos", "todos.db");
|
|
18778
20339
|
}
|
|
18779
20340
|
}
|
|
18780
20341
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
18781
|
-
return
|
|
20342
|
+
return join13(home, ".todos", "todos.db");
|
|
18782
20343
|
}
|
|
18783
20344
|
function ensureDir3(filePath) {
|
|
18784
20345
|
if (isInMemoryDb2(filePath))
|
|
18785
20346
|
return;
|
|
18786
20347
|
const dir = dirname5(resolve3(filePath));
|
|
18787
|
-
if (!
|
|
18788
|
-
|
|
20348
|
+
if (!existsSync7(dir)) {
|
|
20349
|
+
mkdirSync12(dir, { recursive: true });
|
|
18789
20350
|
}
|
|
18790
20351
|
}
|
|
18791
20352
|
function getDatabase3(dbPath) {
|
|
@@ -19244,14 +20805,14 @@ function ensureProject2(name, path, db2) {
|
|
|
19244
20805
|
}
|
|
19245
20806
|
return createProject3({ name, path }, d);
|
|
19246
20807
|
}
|
|
19247
|
-
function
|
|
20808
|
+
function ensureDir23(dir) {
|
|
19248
20809
|
if (!existsSync23(dir))
|
|
19249
20810
|
mkdirSync24(dir, { recursive: true });
|
|
19250
20811
|
}
|
|
19251
20812
|
function listJsonFiles(dir) {
|
|
19252
20813
|
if (!existsSync23(dir))
|
|
19253
20814
|
return [];
|
|
19254
|
-
return
|
|
20815
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".json"));
|
|
19255
20816
|
}
|
|
19256
20817
|
function readJsonFile(path) {
|
|
19257
20818
|
try {
|
|
@@ -19261,7 +20822,7 @@ function readJsonFile(path) {
|
|
|
19261
20822
|
}
|
|
19262
20823
|
}
|
|
19263
20824
|
function writeJsonFile(path, data) {
|
|
19264
|
-
|
|
20825
|
+
writeFileSync6(path, JSON.stringify(data, null, 2) + `
|
|
19265
20826
|
`);
|
|
19266
20827
|
}
|
|
19267
20828
|
function readHighWaterMark(dir) {
|
|
@@ -19272,7 +20833,7 @@ function readHighWaterMark(dir) {
|
|
|
19272
20833
|
return isNaN(val) ? 1 : val;
|
|
19273
20834
|
}
|
|
19274
20835
|
function writeHighWaterMark(dir, value) {
|
|
19275
|
-
|
|
20836
|
+
writeFileSync6(join24(dir, ".highwatermark"), String(value));
|
|
19276
20837
|
}
|
|
19277
20838
|
function getFileMtimeMs(path) {
|
|
19278
20839
|
try {
|
|
@@ -22116,7 +23677,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
|
22116
23677
|
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
22117
23678
|
const dir = getTaskListDir(taskListId);
|
|
22118
23679
|
if (!existsSync43(dir))
|
|
22119
|
-
|
|
23680
|
+
ensureDir23(dir);
|
|
22120
23681
|
const filter = {};
|
|
22121
23682
|
if (projectId)
|
|
22122
23683
|
filter["project_id"] = projectId;
|
|
@@ -22334,7 +23895,7 @@ function metadataKey(agent) {
|
|
|
22334
23895
|
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
22335
23896
|
const dir = getTaskListDir2(agent, taskListId);
|
|
22336
23897
|
if (!existsSync52(dir))
|
|
22337
|
-
|
|
23898
|
+
ensureDir23(dir);
|
|
22338
23899
|
const filter = {};
|
|
22339
23900
|
if (projectId)
|
|
22340
23901
|
filter["project_id"] = projectId;
|
|
@@ -23862,9 +25423,9 @@ __export(exports_dist4, {
|
|
|
23862
25423
|
CATEGORIES: () => CATEGORIES,
|
|
23863
25424
|
AGENT_TARGETS: () => AGENT_TARGETS
|
|
23864
25425
|
});
|
|
23865
|
-
import { existsSync as
|
|
23866
|
-
import { join as
|
|
23867
|
-
import { homedir as
|
|
25426
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync13, writeFileSync as writeFileSync7, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
|
|
25427
|
+
import { join as join14, dirname as dirname6 } from "path";
|
|
25428
|
+
import { homedir as homedir13 } from "os";
|
|
23868
25429
|
import { fileURLToPath } from "url";
|
|
23869
25430
|
import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
|
|
23870
25431
|
import { join as join25 } from "path";
|
|
@@ -23975,35 +25536,35 @@ function normalizeSkillName(name) {
|
|
|
23975
25536
|
function findSkillsDir() {
|
|
23976
25537
|
let dir = __dirname2;
|
|
23977
25538
|
for (let i = 0;i < 5; i++) {
|
|
23978
|
-
const candidate =
|
|
23979
|
-
if (
|
|
25539
|
+
const candidate = join14(dir, "skills");
|
|
25540
|
+
if (existsSync8(candidate)) {
|
|
23980
25541
|
return candidate;
|
|
23981
25542
|
}
|
|
23982
25543
|
dir = dirname6(dir);
|
|
23983
25544
|
}
|
|
23984
|
-
return
|
|
25545
|
+
return join14(__dirname2, "..", "skills");
|
|
23985
25546
|
}
|
|
23986
25547
|
function getSkillPath(name) {
|
|
23987
25548
|
const skillName = normalizeSkillName(name);
|
|
23988
|
-
return
|
|
25549
|
+
return join14(SKILLS_DIR, skillName);
|
|
23989
25550
|
}
|
|
23990
25551
|
function skillExists(name) {
|
|
23991
|
-
return
|
|
25552
|
+
return existsSync8(getSkillPath(name));
|
|
23992
25553
|
}
|
|
23993
25554
|
function installSkill(name, options = {}) {
|
|
23994
25555
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
23995
25556
|
const skillName = normalizeSkillName(name);
|
|
23996
25557
|
const sourcePath = getSkillPath(name);
|
|
23997
|
-
const destDir =
|
|
23998
|
-
const destPath =
|
|
23999
|
-
if (!
|
|
25558
|
+
const destDir = join14(targetDir, ".skills");
|
|
25559
|
+
const destPath = join14(destDir, skillName);
|
|
25560
|
+
if (!existsSync8(sourcePath)) {
|
|
24000
25561
|
return {
|
|
24001
25562
|
skill: name,
|
|
24002
25563
|
success: false,
|
|
24003
25564
|
error: `Skill '${name}' not found`
|
|
24004
25565
|
};
|
|
24005
25566
|
}
|
|
24006
|
-
if (
|
|
25567
|
+
if (existsSync8(destPath) && !overwrite) {
|
|
24007
25568
|
return {
|
|
24008
25569
|
skill: name,
|
|
24009
25570
|
success: false,
|
|
@@ -24012,10 +25573,10 @@ function installSkill(name, options = {}) {
|
|
|
24012
25573
|
};
|
|
24013
25574
|
}
|
|
24014
25575
|
try {
|
|
24015
|
-
if (!
|
|
24016
|
-
|
|
25576
|
+
if (!existsSync8(destDir)) {
|
|
25577
|
+
mkdirSync13(destDir, { recursive: true });
|
|
24017
25578
|
}
|
|
24018
|
-
if (
|
|
25579
|
+
if (existsSync8(destPath) && overwrite) {
|
|
24019
25580
|
rmSync2(destPath, { recursive: true, force: true });
|
|
24020
25581
|
}
|
|
24021
25582
|
cpSync(sourcePath, destPath, {
|
|
@@ -24054,10 +25615,10 @@ function installSkills(names, options = {}) {
|
|
|
24054
25615
|
return names.map((name) => installSkill(name, options));
|
|
24055
25616
|
}
|
|
24056
25617
|
function updateSkillsIndex(skillsDir) {
|
|
24057
|
-
const indexPath =
|
|
25618
|
+
const indexPath = join14(skillsDir, "index.ts");
|
|
24058
25619
|
const meta = loadMeta(skillsDir);
|
|
24059
25620
|
const disabledSet = new Set(meta.disabled || []);
|
|
24060
|
-
const skills =
|
|
25621
|
+
const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
|
|
24061
25622
|
const exports = skills.map((s) => {
|
|
24062
25623
|
const name = s.replace("skill-", "").replace(/-/g, "_");
|
|
24063
25624
|
return `export * as ${name} from './${s}/src/index.js';`;
|
|
@@ -24070,14 +25631,14 @@ function updateSkillsIndex(skillsDir) {
|
|
|
24070
25631
|
|
|
24071
25632
|
${exports}
|
|
24072
25633
|
`;
|
|
24073
|
-
|
|
25634
|
+
writeFileSync7(indexPath, content);
|
|
24074
25635
|
}
|
|
24075
25636
|
function getMetaPath(skillsDir) {
|
|
24076
|
-
return
|
|
25637
|
+
return join14(skillsDir, ".meta.json");
|
|
24077
25638
|
}
|
|
24078
25639
|
function loadMeta(skillsDir) {
|
|
24079
25640
|
const metaPath2 = getMetaPath(skillsDir);
|
|
24080
|
-
if (
|
|
25641
|
+
if (existsSync8(metaPath2)) {
|
|
24081
25642
|
try {
|
|
24082
25643
|
return JSON.parse(readFileSync7(metaPath2, "utf-8"));
|
|
24083
25644
|
} catch {}
|
|
@@ -24085,15 +25646,15 @@ function loadMeta(skillsDir) {
|
|
|
24085
25646
|
return { skills: {} };
|
|
24086
25647
|
}
|
|
24087
25648
|
function saveMeta(skillsDir, meta) {
|
|
24088
|
-
|
|
25649
|
+
writeFileSync7(getMetaPath(skillsDir), JSON.stringify(meta, null, 2));
|
|
24089
25650
|
}
|
|
24090
25651
|
function recordInstall(skillsDir, name) {
|
|
24091
25652
|
const meta = loadMeta(skillsDir);
|
|
24092
25653
|
const skillName = normalizeSkillName(name);
|
|
24093
25654
|
let version = "unknown";
|
|
24094
25655
|
try {
|
|
24095
|
-
const pkgPath =
|
|
24096
|
-
if (
|
|
25656
|
+
const pkgPath = join14(skillsDir, skillName, "package.json");
|
|
25657
|
+
if (existsSync8(pkgPath)) {
|
|
24097
25658
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
24098
25659
|
version = pkg.version || "unknown";
|
|
24099
25660
|
}
|
|
@@ -24107,12 +25668,12 @@ function recordRemove(skillsDir, name) {
|
|
|
24107
25668
|
saveMeta(skillsDir, meta);
|
|
24108
25669
|
}
|
|
24109
25670
|
function getInstallMeta(targetDir = process.cwd()) {
|
|
24110
|
-
return loadMeta(
|
|
25671
|
+
return loadMeta(join14(targetDir, ".skills"));
|
|
24111
25672
|
}
|
|
24112
25673
|
function disableSkill(name, targetDir = process.cwd()) {
|
|
24113
|
-
const skillsDir =
|
|
25674
|
+
const skillsDir = join14(targetDir, ".skills");
|
|
24114
25675
|
const skillName = normalizeSkillName(name);
|
|
24115
|
-
if (!
|
|
25676
|
+
if (!existsSync8(join14(skillsDir, skillName)))
|
|
24116
25677
|
return false;
|
|
24117
25678
|
const meta = loadMeta(skillsDir);
|
|
24118
25679
|
const disabled = new Set(meta.disabled || []);
|
|
@@ -24125,7 +25686,7 @@ function disableSkill(name, targetDir = process.cwd()) {
|
|
|
24125
25686
|
return true;
|
|
24126
25687
|
}
|
|
24127
25688
|
function enableSkill(name, targetDir = process.cwd()) {
|
|
24128
|
-
const skillsDir =
|
|
25689
|
+
const skillsDir = join14(targetDir, ".skills");
|
|
24129
25690
|
const meta = loadMeta(skillsDir);
|
|
24130
25691
|
const disabled = new Set(meta.disabled || []);
|
|
24131
25692
|
if (!disabled.has(name))
|
|
@@ -24137,24 +25698,24 @@ function enableSkill(name, targetDir = process.cwd()) {
|
|
|
24137
25698
|
return true;
|
|
24138
25699
|
}
|
|
24139
25700
|
function getDisabledSkills(targetDir = process.cwd()) {
|
|
24140
|
-
const meta = loadMeta(
|
|
25701
|
+
const meta = loadMeta(join14(targetDir, ".skills"));
|
|
24141
25702
|
return meta.disabled || [];
|
|
24142
25703
|
}
|
|
24143
25704
|
function getInstalledSkills(targetDir = process.cwd()) {
|
|
24144
|
-
const skillsDir =
|
|
24145
|
-
if (!
|
|
25705
|
+
const skillsDir = join14(targetDir, ".skills");
|
|
25706
|
+
if (!existsSync8(skillsDir)) {
|
|
24146
25707
|
return [];
|
|
24147
25708
|
}
|
|
24148
|
-
return
|
|
24149
|
-
const fullPath =
|
|
25709
|
+
return readdirSync6(skillsDir).filter((f) => {
|
|
25710
|
+
const fullPath = join14(skillsDir, f);
|
|
24150
25711
|
return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
|
|
24151
25712
|
}).map((f) => f.replace("skill-", ""));
|
|
24152
25713
|
}
|
|
24153
25714
|
function removeSkill(name, targetDir = process.cwd()) {
|
|
24154
25715
|
const skillName = normalizeSkillName(name);
|
|
24155
|
-
const skillsDir =
|
|
24156
|
-
const skillPath =
|
|
24157
|
-
if (!
|
|
25716
|
+
const skillsDir = join14(targetDir, ".skills");
|
|
25717
|
+
const skillPath = join14(skillsDir, skillName);
|
|
25718
|
+
if (!existsSync8(skillPath)) {
|
|
24158
25719
|
return false;
|
|
24159
25720
|
}
|
|
24160
25721
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
@@ -24165,24 +25726,24 @@ function removeSkill(name, targetDir = process.cwd()) {
|
|
|
24165
25726
|
function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
24166
25727
|
const agentDir = `.${agent}`;
|
|
24167
25728
|
if (scope === "project") {
|
|
24168
|
-
return
|
|
25729
|
+
return join14(projectDir || process.cwd(), agentDir, "skills");
|
|
24169
25730
|
}
|
|
24170
|
-
return
|
|
25731
|
+
return join14(homedir13(), agentDir, "skills");
|
|
24171
25732
|
}
|
|
24172
25733
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
24173
25734
|
const skillName = normalizeSkillName(name);
|
|
24174
|
-
return
|
|
25735
|
+
return join14(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
24175
25736
|
}
|
|
24176
25737
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
24177
25738
|
const { agent, scope = "global", projectDir } = options;
|
|
24178
25739
|
const skillName = normalizeSkillName(name);
|
|
24179
25740
|
const sourcePath = getSkillPath(name);
|
|
24180
|
-
if (!
|
|
25741
|
+
if (!existsSync8(sourcePath)) {
|
|
24181
25742
|
return { skill: name, success: false, error: `Skill '${name}' not found` };
|
|
24182
25743
|
}
|
|
24183
25744
|
let skillMdContent = null;
|
|
24184
|
-
const skillMdPath =
|
|
24185
|
-
if (
|
|
25745
|
+
const skillMdPath = join14(sourcePath, "SKILL.md");
|
|
25746
|
+
if (existsSync8(skillMdPath)) {
|
|
24186
25747
|
skillMdContent = readFileSync7(skillMdPath, "utf-8");
|
|
24187
25748
|
} else if (generateSkillMd) {
|
|
24188
25749
|
skillMdContent = generateSkillMd(name);
|
|
@@ -24192,8 +25753,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24192
25753
|
}
|
|
24193
25754
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
24194
25755
|
if (scope === "global") {
|
|
24195
|
-
const agentBaseDir2 =
|
|
24196
|
-
if (!
|
|
25756
|
+
const agentBaseDir2 = join14(homedir13(), `.${agent}`);
|
|
25757
|
+
if (!existsSync8(agentBaseDir2)) {
|
|
24197
25758
|
const agentLabels = {
|
|
24198
25759
|
claude: "Claude Code",
|
|
24199
25760
|
codex: "Codex CLI",
|
|
@@ -24216,8 +25777,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24216
25777
|
}
|
|
24217
25778
|
}
|
|
24218
25779
|
try {
|
|
24219
|
-
|
|
24220
|
-
|
|
25780
|
+
mkdirSync13(destDir, { recursive: true });
|
|
25781
|
+
writeFileSync7(join14(destDir, "SKILL.md"), skillMdContent);
|
|
24221
25782
|
return { skill: name, success: true, path: destDir };
|
|
24222
25783
|
} catch (error) {
|
|
24223
25784
|
return {
|
|
@@ -24230,7 +25791,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
24230
25791
|
function removeSkillForAgent(name, options) {
|
|
24231
25792
|
const { agent, scope = "global", projectDir } = options;
|
|
24232
25793
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
24233
|
-
if (!
|
|
25794
|
+
if (!existsSync8(destDir)) {
|
|
24234
25795
|
return false;
|
|
24235
25796
|
}
|
|
24236
25797
|
rmSync2(destDir, { recursive: true, force: true });
|
|
@@ -26154,7 +27715,7 @@ __export(exports_cron_manager, {
|
|
|
26154
27715
|
deleteCronJob: () => deleteCronJob,
|
|
26155
27716
|
createCronJob: () => createCronJob
|
|
26156
27717
|
});
|
|
26157
|
-
import { randomUUID as
|
|
27718
|
+
import { randomUUID as randomUUID15 } from "crypto";
|
|
26158
27719
|
function ensureCronTable() {
|
|
26159
27720
|
const db2 = getDatabase();
|
|
26160
27721
|
db2.exec(`
|
|
@@ -26183,7 +27744,7 @@ function ensureCronTable() {
|
|
|
26183
27744
|
function createCronJob(schedule, task, name) {
|
|
26184
27745
|
ensureCronTable();
|
|
26185
27746
|
const db2 = getDatabase();
|
|
26186
|
-
const id =
|
|
27747
|
+
const id = randomUUID15();
|
|
26187
27748
|
db2.prepare(`
|
|
26188
27749
|
INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
|
|
26189
27750
|
VALUES (?, ?, ?, ?, 1)
|
|
@@ -26241,7 +27802,7 @@ function getCronEvents(jobId, limit = 10) {
|
|
|
26241
27802
|
async function executeCronJob(job) {
|
|
26242
27803
|
ensureCronTable();
|
|
26243
27804
|
const db2 = getDatabase();
|
|
26244
|
-
const eventId =
|
|
27805
|
+
const eventId = randomUUID15();
|
|
26245
27806
|
const startedAt = new Date().toISOString();
|
|
26246
27807
|
db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
|
|
26247
27808
|
try {
|
|
@@ -26332,7 +27893,7 @@ __export(exports_url_watcher, {
|
|
|
26332
27893
|
deleteWatchJob: () => deleteWatchJob,
|
|
26333
27894
|
createWatchJob: () => createWatchJob
|
|
26334
27895
|
});
|
|
26335
|
-
import { randomUUID as
|
|
27896
|
+
import { randomUUID as randomUUID16 } from "crypto";
|
|
26336
27897
|
import { createHash } from "crypto";
|
|
26337
27898
|
function ensureWatchTables() {
|
|
26338
27899
|
const db2 = getDatabase();
|
|
@@ -26364,7 +27925,7 @@ function ensureWatchTables() {
|
|
|
26364
27925
|
function createWatchJob(url, schedule, opts) {
|
|
26365
27926
|
ensureWatchTables();
|
|
26366
27927
|
const db2 = getDatabase();
|
|
26367
|
-
const id =
|
|
27928
|
+
const id = randomUUID16();
|
|
26368
27929
|
db2.prepare(`
|
|
26369
27930
|
INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
|
|
26370
27931
|
VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
@@ -26400,7 +27961,7 @@ function getWatchEvents(watchId, limit = 20) {
|
|
|
26400
27961
|
async function checkWatchJob(job) {
|
|
26401
27962
|
ensureWatchTables();
|
|
26402
27963
|
const db2 = getDatabase();
|
|
26403
|
-
const eventId =
|
|
27964
|
+
const eventId = randomUUID16();
|
|
26404
27965
|
const checkedAt = new Date().toISOString();
|
|
26405
27966
|
let newContent = "";
|
|
26406
27967
|
try {
|
|
@@ -30552,23 +32113,23 @@ var NEVER = INVALID;
|
|
|
30552
32113
|
init_session();
|
|
30553
32114
|
init_actions();
|
|
30554
32115
|
import { readFileSync as readFileSync8 } from "fs";
|
|
30555
|
-
import { join as
|
|
32116
|
+
import { join as join15 } from "path";
|
|
30556
32117
|
|
|
30557
32118
|
// src/lib/screenshot.ts
|
|
30558
32119
|
init_types();
|
|
30559
32120
|
init_gallery();
|
|
30560
32121
|
var import_sharp = __toESM(require_lib(), 1);
|
|
30561
|
-
import { join as
|
|
30562
|
-
import { mkdirSync as
|
|
30563
|
-
import { homedir as
|
|
32122
|
+
import { join as join4 } from "path";
|
|
32123
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
32124
|
+
import { homedir as homedir4 } from "os";
|
|
30564
32125
|
function getDataDir2() {
|
|
30565
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
32126
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
30566
32127
|
}
|
|
30567
32128
|
function getScreenshotDir(projectId) {
|
|
30568
|
-
const base =
|
|
32129
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
30569
32130
|
const date = new Date().toISOString().split("T")[0];
|
|
30570
|
-
const dir = projectId ?
|
|
30571
|
-
|
|
32131
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
32132
|
+
mkdirSync4(dir, { recursive: true });
|
|
30572
32133
|
return dir;
|
|
30573
32134
|
}
|
|
30574
32135
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -30583,7 +32144,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
30583
32144
|
}
|
|
30584
32145
|
}
|
|
30585
32146
|
async function generateThumbnail(raw, dir, stem) {
|
|
30586
|
-
const thumbPath =
|
|
32147
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
30587
32148
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
30588
32149
|
await Bun.write(thumbPath, thumbBuffer);
|
|
30589
32150
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -30640,7 +32201,7 @@ async function takeScreenshot(page, opts) {
|
|
|
30640
32201
|
const compressedSizeBytes = finalBuffer.length;
|
|
30641
32202
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
30642
32203
|
const ext = format;
|
|
30643
|
-
const screenshotPath = opts?.path ??
|
|
32204
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
30644
32205
|
await Bun.write(screenshotPath, finalBuffer);
|
|
30645
32206
|
let thumbnailPath;
|
|
30646
32207
|
let thumbnailBase64;
|
|
@@ -30700,12 +32261,12 @@ async function takeScreenshot(page, opts) {
|
|
|
30700
32261
|
}
|
|
30701
32262
|
async function generatePDF(page, opts) {
|
|
30702
32263
|
try {
|
|
30703
|
-
const base =
|
|
32264
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
30704
32265
|
const date = new Date().toISOString().split("T")[0];
|
|
30705
|
-
const dir = opts?.projectId ?
|
|
30706
|
-
|
|
32266
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
32267
|
+
mkdirSync4(dir, { recursive: true });
|
|
30707
32268
|
const timestamp = Date.now();
|
|
30708
|
-
const pdfPath = opts?.path ??
|
|
32269
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
30709
32270
|
const buffer = await page.pdf({
|
|
30710
32271
|
path: pdfPath,
|
|
30711
32272
|
format: opts?.format ?? "A4",
|
|
@@ -30726,118 +32287,8 @@ async function generatePDF(page, opts) {
|
|
|
30726
32287
|
// src/mcp/index.ts
|
|
30727
32288
|
init_network();
|
|
30728
32289
|
|
|
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
32290
|
// src/lib/performance.ts
|
|
32291
|
+
init_cdp();
|
|
30841
32292
|
async function getPerformanceMetrics(page) {
|
|
30842
32293
|
const navTiming = await page.evaluate(() => {
|
|
30843
32294
|
const t = performance.timing;
|
|
@@ -30929,144 +32380,8 @@ async function setSessionStorage(page, key, value) {
|
|
|
30929
32380
|
await page.evaluate(([k, v]) => sessionStorage.setItem(k, v), [key, value]);
|
|
30930
32381
|
}
|
|
30931
32382
|
|
|
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
|
-
}
|
|
32383
|
+
// src/mcp/index.ts
|
|
32384
|
+
init_recorder();
|
|
31070
32385
|
|
|
31071
32386
|
// src/lib/crawler.ts
|
|
31072
32387
|
init_types();
|
|
@@ -31249,16 +32564,16 @@ init_gallery();
|
|
|
31249
32564
|
|
|
31250
32565
|
// src/lib/downloads.ts
|
|
31251
32566
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
31252
|
-
import { join as
|
|
31253
|
-
import { mkdirSync as
|
|
31254
|
-
import { homedir as
|
|
32567
|
+
import { join as join5, basename, extname } from "path";
|
|
32568
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
32569
|
+
import { homedir as homedir5 } from "os";
|
|
31255
32570
|
function getDataDir3() {
|
|
31256
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
32571
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
31257
32572
|
}
|
|
31258
32573
|
function getDownloadsDir(sessionId) {
|
|
31259
|
-
const base =
|
|
31260
|
-
const dir = sessionId ?
|
|
31261
|
-
|
|
32574
|
+
const base = join5(getDataDir3(), "downloads");
|
|
32575
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
32576
|
+
mkdirSync5(dir, { recursive: true });
|
|
31262
32577
|
return dir;
|
|
31263
32578
|
}
|
|
31264
32579
|
function metaPath(filePath) {
|
|
@@ -31270,7 +32585,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
31270
32585
|
const ext = extname(filename) || "";
|
|
31271
32586
|
const stem = basename(filename, ext);
|
|
31272
32587
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
31273
|
-
const filePath =
|
|
32588
|
+
const filePath = join5(dir, uniqueName);
|
|
31274
32589
|
writeFileSync(filePath, buffer);
|
|
31275
32590
|
const meta = {
|
|
31276
32591
|
id,
|
|
@@ -31299,20 +32614,20 @@ function listDownloads(sessionId) {
|
|
|
31299
32614
|
const dir = getDownloadsDir(sessionId);
|
|
31300
32615
|
const results = [];
|
|
31301
32616
|
function scanDir(d) {
|
|
31302
|
-
if (!
|
|
32617
|
+
if (!existsSync2(d))
|
|
31303
32618
|
return;
|
|
31304
|
-
const entries =
|
|
32619
|
+
const entries = readdirSync2(d);
|
|
31305
32620
|
for (const entry of entries) {
|
|
31306
32621
|
if (entry.endsWith(".meta.json"))
|
|
31307
32622
|
continue;
|
|
31308
|
-
const full =
|
|
32623
|
+
const full = join5(d, entry);
|
|
31309
32624
|
const stat = statSync(full);
|
|
31310
32625
|
if (stat.isDirectory()) {
|
|
31311
32626
|
scanDir(full);
|
|
31312
32627
|
continue;
|
|
31313
32628
|
}
|
|
31314
32629
|
const mpath = metaPath(full);
|
|
31315
|
-
if (!
|
|
32630
|
+
if (!existsSync2(mpath))
|
|
31316
32631
|
continue;
|
|
31317
32632
|
try {
|
|
31318
32633
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -31342,9 +32657,9 @@ function deleteDownload(id, sessionId) {
|
|
|
31342
32657
|
if (!file)
|
|
31343
32658
|
return false;
|
|
31344
32659
|
try {
|
|
31345
|
-
|
|
31346
|
-
if (
|
|
31347
|
-
|
|
32660
|
+
unlinkSync2(file.path);
|
|
32661
|
+
if (existsSync2(file.meta_path))
|
|
32662
|
+
unlinkSync2(file.meta_path);
|
|
31348
32663
|
return true;
|
|
31349
32664
|
} catch {
|
|
31350
32665
|
return false;
|
|
@@ -31390,9 +32705,9 @@ function detectType(filename) {
|
|
|
31390
32705
|
|
|
31391
32706
|
// src/lib/gallery-diff.ts
|
|
31392
32707
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
31393
|
-
import { join as
|
|
31394
|
-
import { mkdirSync as
|
|
31395
|
-
import { homedir as
|
|
32708
|
+
import { join as join6 } from "path";
|
|
32709
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
32710
|
+
import { homedir as homedir6 } from "os";
|
|
31396
32711
|
async function diffImages(path1, path2) {
|
|
31397
32712
|
const img1 = import_sharp2.default(path1);
|
|
31398
32713
|
const img2 = import_sharp2.default(path2);
|
|
@@ -31423,10 +32738,10 @@ async function diffImages(path1, path2) {
|
|
|
31423
32738
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
31424
32739
|
}
|
|
31425
32740
|
}
|
|
31426
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
31427
|
-
const diffDir =
|
|
31428
|
-
|
|
31429
|
-
const diffPath =
|
|
32741
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
32742
|
+
const diffDir = join6(dataDir, "diffs");
|
|
32743
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
32744
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
31430
32745
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
31431
32746
|
await Bun.write(diffPath, diffImageBuffer);
|
|
31432
32747
|
return {
|
|
@@ -31442,9 +32757,9 @@ async function diffImages(path1, path2) {
|
|
|
31442
32757
|
init_snapshot();
|
|
31443
32758
|
|
|
31444
32759
|
// src/lib/files-integration.ts
|
|
31445
|
-
import { join as
|
|
31446
|
-
import { mkdirSync as
|
|
31447
|
-
import { homedir as
|
|
32760
|
+
import { join as join7 } from "path";
|
|
32761
|
+
import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
|
|
32762
|
+
import { homedir as homedir7 } from "os";
|
|
31448
32763
|
async function persistFile(localPath, opts) {
|
|
31449
32764
|
try {
|
|
31450
32765
|
const mod = await import("@hasna/files");
|
|
@@ -31453,12 +32768,12 @@ async function persistFile(localPath, opts) {
|
|
|
31453
32768
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
31454
32769
|
}
|
|
31455
32770
|
} catch {}
|
|
31456
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
32771
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
31457
32772
|
const date = new Date().toISOString().split("T")[0];
|
|
31458
|
-
const dir =
|
|
31459
|
-
|
|
32773
|
+
const dir = join7(dataDir, "persistent", date);
|
|
32774
|
+
mkdirSync7(dir, { recursive: true });
|
|
31460
32775
|
const filename = localPath.split("/").pop() ?? "file";
|
|
31461
|
-
const targetPath =
|
|
32776
|
+
const targetPath = join7(dir, filename);
|
|
31462
32777
|
copyFileSync2(localPath, targetPath);
|
|
31463
32778
|
return {
|
|
31464
32779
|
id: `local-${Date.now()}`,
|
|
@@ -31468,6 +32783,9 @@ async function persistFile(localPath, opts) {
|
|
|
31468
32783
|
};
|
|
31469
32784
|
}
|
|
31470
32785
|
|
|
32786
|
+
// src/mcp/index.ts
|
|
32787
|
+
init_recordings();
|
|
32788
|
+
|
|
31471
32789
|
// src/db/timeline.ts
|
|
31472
32790
|
init_schema();
|
|
31473
32791
|
function logEvent(sessionId, eventType, details = {}) {
|
|
@@ -31565,7 +32883,7 @@ async function closeTab(page, index) {
|
|
|
31565
32883
|
init_dialogs();
|
|
31566
32884
|
init_profiles();
|
|
31567
32885
|
init_types();
|
|
31568
|
-
var _pkg = JSON.parse(readFileSync8(
|
|
32886
|
+
var _pkg = JSON.parse(readFileSync8(join15(import.meta.dir, "../../package.json"), "utf8"));
|
|
31569
32887
|
var networkLogCleanup = new Map;
|
|
31570
32888
|
var consoleCaptureCleanup = new Map;
|
|
31571
32889
|
var harCaptures = new Map;
|
|
@@ -31612,7 +32930,7 @@ var server = new McpServer({
|
|
|
31612
32930
|
name: "@hasna/browser",
|
|
31613
32931
|
version: "0.0.1"
|
|
31614
32932
|
});
|
|
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.", {
|
|
32933
|
+
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
32934
|
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
31617
32935
|
use_case: exports_external.string().optional(),
|
|
31618
32936
|
project_id: exports_external.string().optional(),
|
|
@@ -31623,9 +32941,11 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
|
|
|
31623
32941
|
viewport_height: exports_external.number().optional().default(720),
|
|
31624
32942
|
stealth: exports_external.boolean().optional().default(false),
|
|
31625
32943
|
auto_gallery: exports_external.boolean().optional().default(false),
|
|
32944
|
+
storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
|
|
31626
32945
|
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
|
-
|
|
32946
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
32947
|
+
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")
|
|
32948
|
+
}, 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
32949
|
try {
|
|
31630
32950
|
if (agent_id && !force_new) {
|
|
31631
32951
|
const existing = getActiveSessionForAgent2(agent_id);
|
|
@@ -31641,7 +32961,9 @@ server.tool("browser_session_create", "Create a new browser session. If agent_id
|
|
|
31641
32961
|
headless,
|
|
31642
32962
|
viewport: { width: viewport_width, height: viewport_height },
|
|
31643
32963
|
stealth,
|
|
31644
|
-
autoGallery: auto_gallery
|
|
32964
|
+
autoGallery: auto_gallery,
|
|
32965
|
+
storageState: storage_state,
|
|
32966
|
+
cdpUrl: cdp_url
|
|
31645
32967
|
});
|
|
31646
32968
|
if (tags?.length) {
|
|
31647
32969
|
const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -31801,7 +33123,7 @@ server.tool("browser_reload", "Reload the current page", { session_id: exports_e
|
|
|
31801
33123
|
return err(e);
|
|
31802
33124
|
}
|
|
31803
33125
|
});
|
|
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 }) => {
|
|
33126
|
+
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
33127
|
try {
|
|
31806
33128
|
const sid = resolveSessionId(session_id);
|
|
31807
33129
|
const page = getSessionPage(sid);
|
|
@@ -31812,14 +33134,17 @@ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS sel
|
|
|
31812
33134
|
}
|
|
31813
33135
|
if (!selector)
|
|
31814
33136
|
return err(new Error("Either ref or selector is required"));
|
|
31815
|
-
await click(page, selector, { button, timeout });
|
|
31816
|
-
logEvent(sid, "click", { selector, method: "selector" });
|
|
33137
|
+
const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
|
|
33138
|
+
logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
|
|
33139
|
+
if (healInfo.healed) {
|
|
33140
|
+
return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
33141
|
+
}
|
|
31817
33142
|
return json({ clicked: selector, method: "selector" });
|
|
31818
33143
|
} catch (e) {
|
|
31819
33144
|
return errWithScreenshot(e, session_id);
|
|
31820
33145
|
}
|
|
31821
33146
|
});
|
|
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 }) => {
|
|
33147
|
+
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
33148
|
try {
|
|
31824
33149
|
const sid = resolveSessionId(session_id);
|
|
31825
33150
|
const page = getSessionPage(sid);
|
|
@@ -31830,8 +33155,11 @@ server.tool("browser_type", "Type text into an element by ref or selector. Prefe
|
|
|
31830
33155
|
}
|
|
31831
33156
|
if (!selector)
|
|
31832
33157
|
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) });
|
|
33158
|
+
const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
|
|
33159
|
+
logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
|
|
33160
|
+
if (healInfo.healed) {
|
|
33161
|
+
return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
33162
|
+
}
|
|
31835
33163
|
return json({ typed: text, selector, method: "selector" });
|
|
31836
33164
|
} catch (e) {
|
|
31837
33165
|
return errWithScreenshot(e, session_id);
|
|
@@ -31925,20 +33253,32 @@ server.tool("browser_wait", "Wait for a selector to appear", { session_id: expor
|
|
|
31925
33253
|
return err(e);
|
|
31926
33254
|
}
|
|
31927
33255
|
});
|
|
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 }) => {
|
|
33256
|
+
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
33257
|
try {
|
|
31930
33258
|
const sid = resolveSessionId(session_id);
|
|
31931
33259
|
const page = getSessionPage(sid);
|
|
31932
|
-
|
|
33260
|
+
const text = await getText(page, selector);
|
|
33261
|
+
if (sanitize) {
|
|
33262
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
33263
|
+
const sanitized = sanitizeText2(text);
|
|
33264
|
+
return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
33265
|
+
}
|
|
33266
|
+
return json({ text });
|
|
31933
33267
|
} catch (e) {
|
|
31934
33268
|
return err(e);
|
|
31935
33269
|
}
|
|
31936
33270
|
});
|
|
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 }) => {
|
|
33271
|
+
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
33272
|
try {
|
|
31939
33273
|
const sid = resolveSessionId(session_id);
|
|
31940
33274
|
const page = getSessionPage(sid);
|
|
31941
|
-
|
|
33275
|
+
const html = await getHTML(page, selector);
|
|
33276
|
+
if (sanitize) {
|
|
33277
|
+
const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
33278
|
+
const sanitized = sanitizeHTML2(html);
|
|
33279
|
+
return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
33280
|
+
}
|
|
33281
|
+
return json({ html });
|
|
31942
33282
|
} catch (e) {
|
|
31943
33283
|
return err(e);
|
|
31944
33284
|
}
|
|
@@ -31953,16 +33293,32 @@ server.tool("browser_get_links", "Get all links from the current page", { sessio
|
|
|
31953
33293
|
return err(e);
|
|
31954
33294
|
}
|
|
31955
33295
|
});
|
|
31956
|
-
server.tool("browser_extract", "Extract content from the page in a specified format", {
|
|
33296
|
+
server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
|
|
31957
33297
|
session_id: exports_external.string().optional(),
|
|
31958
33298
|
format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
|
|
31959
33299
|
selector: exports_external.string().optional(),
|
|
31960
|
-
schema: exports_external.record(exports_external.string()).optional()
|
|
31961
|
-
|
|
33300
|
+
schema: exports_external.record(exports_external.string()).optional(),
|
|
33301
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
|
|
33302
|
+
}, async ({ session_id, format, selector, schema, sanitize }) => {
|
|
31962
33303
|
try {
|
|
31963
33304
|
const sid = resolveSessionId(session_id);
|
|
31964
33305
|
const page = getSessionPage(sid);
|
|
31965
33306
|
const result = await extract(page, { format, selector, schema });
|
|
33307
|
+
if (sanitize) {
|
|
33308
|
+
const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
33309
|
+
if (result.text) {
|
|
33310
|
+
const s = sanitizeText2(result.text);
|
|
33311
|
+
result.text = s.text;
|
|
33312
|
+
result.stripped = s.stripped;
|
|
33313
|
+
result.warnings = s.warnings;
|
|
33314
|
+
}
|
|
33315
|
+
if (result.html) {
|
|
33316
|
+
const s = sanitizeHTML2(result.html);
|
|
33317
|
+
result.html = s.text;
|
|
33318
|
+
result.stripped = s.stripped;
|
|
33319
|
+
result.warnings = s.warnings;
|
|
33320
|
+
}
|
|
33321
|
+
}
|
|
31966
33322
|
return json(result);
|
|
31967
33323
|
} catch (e) {
|
|
31968
33324
|
return err(e);
|
|
@@ -31979,17 +33335,27 @@ server.tool("browser_find", "Find elements matching a selector and return their
|
|
|
31979
33335
|
return err(e);
|
|
31980
33336
|
}
|
|
31981
33337
|
});
|
|
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.", {
|
|
33338
|
+
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
33339
|
session_id: exports_external.string().optional(),
|
|
31984
33340
|
compact: exports_external.boolean().optional().default(true),
|
|
31985
33341
|
max_refs: exports_external.number().optional().default(50),
|
|
31986
|
-
full_tree: exports_external.boolean().optional().default(false)
|
|
31987
|
-
|
|
33342
|
+
full_tree: exports_external.boolean().optional().default(false),
|
|
33343
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
|
|
33344
|
+
}, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
|
|
31988
33345
|
try {
|
|
31989
33346
|
const sid = resolveSessionId(session_id);
|
|
31990
33347
|
const page = getSessionPage(sid);
|
|
31991
33348
|
const result = await takeSnapshot(page, sid);
|
|
31992
33349
|
setLastSnapshot(sid, result);
|
|
33350
|
+
let injection_warnings;
|
|
33351
|
+
if (sanitize) {
|
|
33352
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
33353
|
+
const sanitized = sanitizeText2(result.tree);
|
|
33354
|
+
if (sanitized.stripped > 0) {
|
|
33355
|
+
injection_warnings = sanitized.warnings;
|
|
33356
|
+
result.tree = sanitized.text;
|
|
33357
|
+
}
|
|
33358
|
+
}
|
|
31993
33359
|
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
31994
33360
|
const limitedRefs = Object.fromEntries(refEntries);
|
|
31995
33361
|
const truncated = Object.keys(result.refs).length > max_refs;
|
|
@@ -32001,27 +33367,29 @@ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@
|
|
|
32001
33367
|
interactive_count: result.interactive_count,
|
|
32002
33368
|
shown_count: refEntries.length,
|
|
32003
33369
|
truncated,
|
|
32004
|
-
refs: limitedRefs
|
|
33370
|
+
refs: limitedRefs,
|
|
33371
|
+
...injection_warnings ? { injection_warnings } : {}
|
|
32005
33372
|
});
|
|
32006
33373
|
}
|
|
32007
33374
|
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
32008
33375
|
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
32009
|
-
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
33376
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
|
|
32010
33377
|
} catch (e) {
|
|
32011
33378
|
return err(e);
|
|
32012
33379
|
}
|
|
32013
33380
|
});
|
|
32014
|
-
server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements
|
|
33381
|
+
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
33382
|
session_id: exports_external.string().optional(),
|
|
32016
|
-
selector: exports_external.string().optional(),
|
|
33383
|
+
selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
|
|
32017
33384
|
full_page: exports_external.boolean().optional().default(false),
|
|
32018
33385
|
format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
|
|
32019
33386
|
quality: exports_external.number().optional().default(60),
|
|
32020
33387
|
max_width: exports_external.number().optional().default(800),
|
|
32021
33388
|
compress: exports_external.boolean().optional().default(true),
|
|
32022
33389
|
thumbnail: exports_external.boolean().optional().default(true),
|
|
32023
|
-
annotate: exports_external.boolean().optional().default(false)
|
|
32024
|
-
|
|
33390
|
+
annotate: exports_external.boolean().optional().default(false),
|
|
33391
|
+
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).")
|
|
33392
|
+
}, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
|
|
32025
33393
|
try {
|
|
32026
33394
|
const sid = resolveSessionId(session_id);
|
|
32027
33395
|
const page = getSessionPage(sid);
|
|
@@ -32038,7 +33406,9 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
|
|
|
32038
33406
|
annotation_count: annotated.annotations.length
|
|
32039
33407
|
});
|
|
32040
33408
|
}
|
|
32041
|
-
const
|
|
33409
|
+
const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
|
|
33410
|
+
const effectiveQuality = detail === "high" ? 75 : quality;
|
|
33411
|
+
const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
|
|
32042
33412
|
result.url = page.url();
|
|
32043
33413
|
try {
|
|
32044
33414
|
const buf = Buffer.from(result.base64, "base64");
|
|
@@ -32047,12 +33417,12 @@ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overl
|
|
|
32047
33417
|
result.download_id = dl.id;
|
|
32048
33418
|
} catch {}
|
|
32049
33419
|
result.estimated_tokens = Math.ceil(result.base64.length / 4);
|
|
32050
|
-
if (result.base64.length >
|
|
33420
|
+
if (detail !== "high" && result.base64.length > 40000) {
|
|
32051
33421
|
result.base64_truncated = true;
|
|
32052
33422
|
result.full_image_path = result.path;
|
|
32053
33423
|
result.base64 = result.thumbnail_base64 ?? "";
|
|
32054
33424
|
}
|
|
32055
|
-
logEvent(sid, "screenshot", { path: result.path });
|
|
33425
|
+
logEvent(sid, "screenshot", { path: result.path, detail, selector });
|
|
32056
33426
|
return json(result);
|
|
32057
33427
|
} catch (e) {
|
|
32058
33428
|
return err(e);
|
|
@@ -32234,6 +33604,28 @@ server.tool("browser_performance", "Get performance metrics for the current page
|
|
|
32234
33604
|
return err(e);
|
|
32235
33605
|
}
|
|
32236
33606
|
});
|
|
33607
|
+
server.tool("browser_detect_env", "Detect if the current page is running in production, development, staging, or local environment. Analyzes URL, meta tags, source maps, analytics SDKs, and more.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
33608
|
+
try {
|
|
33609
|
+
const sid = resolveSessionId(session_id);
|
|
33610
|
+
const page = getSessionPage(sid);
|
|
33611
|
+
const { detectEnvironment: detectEnvironment2 } = await Promise.resolve().then(() => exports_env_detector);
|
|
33612
|
+
const result = await detectEnvironment2(page);
|
|
33613
|
+
return json(result);
|
|
33614
|
+
} catch (e) {
|
|
33615
|
+
return err(e);
|
|
33616
|
+
}
|
|
33617
|
+
});
|
|
33618
|
+
server.tool("browser_performance_deep", "Deep performance analysis: Web Vitals, resource breakdown by type, largest resources, third-party scripts with categories, DOM complexity, memory usage.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
33619
|
+
try {
|
|
33620
|
+
const sid = resolveSessionId(session_id);
|
|
33621
|
+
const page = getSessionPage(sid);
|
|
33622
|
+
const { getDeepPerformance: getDeepPerformance2 } = await Promise.resolve().then(() => (init_deep_performance(), exports_deep_performance));
|
|
33623
|
+
const result = await getDeepPerformance2(page);
|
|
33624
|
+
return json(result);
|
|
33625
|
+
} catch (e) {
|
|
33626
|
+
return err(e);
|
|
33627
|
+
}
|
|
33628
|
+
});
|
|
32237
33629
|
server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
|
|
32238
33630
|
try {
|
|
32239
33631
|
const sid = resolveSessionId(session_id);
|
|
@@ -32297,6 +33689,46 @@ server.tool("browser_recordings_list", "List all recordings", { project_id: expo
|
|
|
32297
33689
|
return err(e);
|
|
32298
33690
|
}
|
|
32299
33691
|
});
|
|
33692
|
+
server.tool("browser_workflow_save", "Save a recording as a reusable workflow with self-healing replay", { recording_id: exports_external.string(), name: exports_external.string(), description: exports_external.string().optional() }, async ({ recording_id, name, description }) => {
|
|
33693
|
+
try {
|
|
33694
|
+
const { saveWorkflowFromRecording: saveWorkflowFromRecording2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
|
|
33695
|
+
return json(saveWorkflowFromRecording2(recording_id, name, description));
|
|
33696
|
+
} catch (e) {
|
|
33697
|
+
return err(e);
|
|
33698
|
+
}
|
|
33699
|
+
});
|
|
33700
|
+
server.tool("browser_workflow_list", "List all saved workflows", {}, async () => {
|
|
33701
|
+
try {
|
|
33702
|
+
const { listWorkflows: listWorkflows2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
|
|
33703
|
+
const workflows = listWorkflows2();
|
|
33704
|
+
return json({ workflows: workflows.map((w) => ({ ...w, steps: `${w.steps.length} steps` })), count: workflows.length });
|
|
33705
|
+
} catch (e) {
|
|
33706
|
+
return err(e);
|
|
33707
|
+
}
|
|
33708
|
+
});
|
|
33709
|
+
server.tool("browser_workflow_run", "Run a saved workflow with self-healing. If selectors changed, auto-adapts and reports what was healed.", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
|
|
33710
|
+
try {
|
|
33711
|
+
const sid = resolveSessionId(session_id);
|
|
33712
|
+
const page = getSessionPage(sid);
|
|
33713
|
+
const { getWorkflowByName: getWorkflowByName2, runWorkflow: runWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
|
|
33714
|
+
const workflow = getWorkflowByName2(name);
|
|
33715
|
+
if (!workflow)
|
|
33716
|
+
return err(new Error(`Workflow '${name}' not found`));
|
|
33717
|
+
const result = await runWorkflow2(workflow, page);
|
|
33718
|
+
logEvent(sid, "workflow_run", { name, ...result });
|
|
33719
|
+
return json(result);
|
|
33720
|
+
} catch (e) {
|
|
33721
|
+
return err(e);
|
|
33722
|
+
}
|
|
33723
|
+
});
|
|
33724
|
+
server.tool("browser_workflow_delete", "Delete a saved workflow", { name: exports_external.string() }, async ({ name }) => {
|
|
33725
|
+
try {
|
|
33726
|
+
const { deleteWorkflow: deleteWorkflow2 } = await Promise.resolve().then(() => (init_workflows(), exports_workflows));
|
|
33727
|
+
return json({ deleted: deleteWorkflow2(name) });
|
|
33728
|
+
} catch (e) {
|
|
33729
|
+
return err(e);
|
|
33730
|
+
}
|
|
33731
|
+
});
|
|
32300
33732
|
server.tool("browser_crawl", "Crawl a URL recursively and return discovered pages", {
|
|
32301
33733
|
url: exports_external.string(),
|
|
32302
33734
|
max_depth: exports_external.number().optional().default(2),
|
|
@@ -32458,6 +33890,94 @@ server.tool("browser_session_untag", "Remove a tag from a session", { session_id
|
|
|
32458
33890
|
return err(e);
|
|
32459
33891
|
}
|
|
32460
33892
|
});
|
|
33893
|
+
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 }) => {
|
|
33894
|
+
try {
|
|
33895
|
+
const sid = resolveSessionId(session_id);
|
|
33896
|
+
const page = getSessionPage(sid);
|
|
33897
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33898
|
+
const path = await saveStateFromPage2(page, name);
|
|
33899
|
+
return json({ saved: true, name, path });
|
|
33900
|
+
} catch (e) {
|
|
33901
|
+
return err(e);
|
|
33902
|
+
}
|
|
33903
|
+
});
|
|
33904
|
+
server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
|
|
33905
|
+
try {
|
|
33906
|
+
const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33907
|
+
const states = listStates2();
|
|
33908
|
+
return json({ states, count: states.length });
|
|
33909
|
+
} catch (e) {
|
|
33910
|
+
return err(e);
|
|
33911
|
+
}
|
|
33912
|
+
});
|
|
33913
|
+
server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
|
|
33914
|
+
try {
|
|
33915
|
+
const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33916
|
+
return json({ deleted: deleteState2(name), name });
|
|
33917
|
+
} catch (e) {
|
|
33918
|
+
return err(e);
|
|
33919
|
+
}
|
|
33920
|
+
});
|
|
33921
|
+
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 }) => {
|
|
33922
|
+
try {
|
|
33923
|
+
const sid = resolveSessionId(session_id);
|
|
33924
|
+
const page = getSessionPage(sid);
|
|
33925
|
+
if (start_url)
|
|
33926
|
+
await navigate(page, start_url);
|
|
33927
|
+
const recording = startRecording(sid, `auth-${name}`, page.url());
|
|
33928
|
+
return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
|
|
33929
|
+
} catch (e) {
|
|
33930
|
+
return err(e);
|
|
33931
|
+
}
|
|
33932
|
+
});
|
|
33933
|
+
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 }) => {
|
|
33934
|
+
try {
|
|
33935
|
+
const sid = resolveSessionId(session_id);
|
|
33936
|
+
const page = getSessionPage(sid);
|
|
33937
|
+
const recording = stopRecording(recording_id);
|
|
33938
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33939
|
+
const statePath2 = await saveStateFromPage2(page, name);
|
|
33940
|
+
let domain = "";
|
|
33941
|
+
try {
|
|
33942
|
+
domain = new URL(page.url()).hostname;
|
|
33943
|
+
} catch {}
|
|
33944
|
+
const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33945
|
+
const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
|
|
33946
|
+
return json({ flow, recording_steps: recording.steps.length });
|
|
33947
|
+
} catch (e) {
|
|
33948
|
+
return err(e);
|
|
33949
|
+
}
|
|
33950
|
+
});
|
|
33951
|
+
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 }) => {
|
|
33952
|
+
try {
|
|
33953
|
+
const sid = resolveSessionId(session_id);
|
|
33954
|
+
const page = getSessionPage(sid);
|
|
33955
|
+
const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33956
|
+
const flow = getAuthFlowByName2(name);
|
|
33957
|
+
if (!flow)
|
|
33958
|
+
return err(new Error(`Auth flow '${name}' not found`));
|
|
33959
|
+
const result = await tryReplayAuth2(page, flow.domain);
|
|
33960
|
+
return json(result);
|
|
33961
|
+
} catch (e) {
|
|
33962
|
+
return err(e);
|
|
33963
|
+
}
|
|
33964
|
+
});
|
|
33965
|
+
server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
|
|
33966
|
+
try {
|
|
33967
|
+
const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33968
|
+
return json({ flows: listAuthFlows2() });
|
|
33969
|
+
} catch (e) {
|
|
33970
|
+
return err(e);
|
|
33971
|
+
}
|
|
33972
|
+
});
|
|
33973
|
+
server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
|
|
33974
|
+
try {
|
|
33975
|
+
const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
33976
|
+
return json({ deleted: deleteAuthFlow2(name) });
|
|
33977
|
+
} catch (e) {
|
|
33978
|
+
return err(e);
|
|
33979
|
+
}
|
|
33980
|
+
});
|
|
32461
33981
|
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
33982
|
try {
|
|
32463
33983
|
const sid = resolveSessionId(session_id);
|
|
@@ -32468,20 +33988,45 @@ server.tool("browser_click_text", "Click an element by its visible text content"
|
|
|
32468
33988
|
return err(e);
|
|
32469
33989
|
}
|
|
32470
33990
|
});
|
|
32471
|
-
server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
|
|
33991
|
+
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
33992
|
session_id: exports_external.string().optional(),
|
|
32473
33993
|
fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
|
|
32474
|
-
submit_selector: exports_external.string().optional()
|
|
32475
|
-
|
|
33994
|
+
submit_selector: exports_external.string().optional(),
|
|
33995
|
+
self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
|
|
33996
|
+
}, async ({ session_id, fields, submit_selector, self_heal }) => {
|
|
32476
33997
|
try {
|
|
32477
33998
|
const sid = resolveSessionId(session_id);
|
|
32478
33999
|
const page = getSessionPage(sid);
|
|
32479
|
-
const result = await fillForm(page, fields, submit_selector);
|
|
34000
|
+
const result = await fillForm(page, fields, submit_selector, self_heal);
|
|
32480
34001
|
return json(result);
|
|
32481
34002
|
} catch (e) {
|
|
32482
34003
|
return errWithScreenshot(e, session_id);
|
|
32483
34004
|
}
|
|
32484
34005
|
});
|
|
34006
|
+
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.", {
|
|
34007
|
+
session_id: exports_external.string().optional(),
|
|
34008
|
+
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')"),
|
|
34009
|
+
click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
|
|
34010
|
+
model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
|
|
34011
|
+
}, async ({ session_id, description, click: doClick, model }) => {
|
|
34012
|
+
try {
|
|
34013
|
+
const sid = resolveSessionId(session_id);
|
|
34014
|
+
const page = getSessionPage(sid);
|
|
34015
|
+
if (doClick) {
|
|
34016
|
+
const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
34017
|
+
const result = await clickByVision2(page, description, { model });
|
|
34018
|
+
logEvent(sid, "vision_click", { query: description, ...result });
|
|
34019
|
+
return json(result);
|
|
34020
|
+
} else {
|
|
34021
|
+
const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
34022
|
+
const result = await findElementByVision2(page, description, { model });
|
|
34023
|
+
logEvent(sid, "vision_find", { query: description, ...result });
|
|
34024
|
+
return json(result);
|
|
34025
|
+
}
|
|
34026
|
+
} catch (e) {
|
|
34027
|
+
return err(e);
|
|
34028
|
+
}
|
|
34029
|
+
});
|
|
32485
34030
|
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
34031
|
try {
|
|
32487
34032
|
const sid = resolveSessionId(session_id);
|
|
@@ -32879,6 +34424,68 @@ server.tool("browser_profile_delete", "Delete a saved browser profile", { name:
|
|
|
32879
34424
|
return err(e);
|
|
32880
34425
|
}
|
|
32881
34426
|
});
|
|
34427
|
+
server.tool("browser_detect_apis", "Scan network traffic for JSON API endpoints. Returns discovered endpoints with methods, status codes, and URLs.", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
34428
|
+
try {
|
|
34429
|
+
const sid = resolveSessionId(session_id);
|
|
34430
|
+
const { detectAPIs: detectAPIs2 } = await Promise.resolve().then(() => (init_api_detector(), exports_api_detector));
|
|
34431
|
+
const apis = detectAPIs2(sid);
|
|
34432
|
+
return json({ apis, count: apis.length });
|
|
34433
|
+
} catch (e) {
|
|
34434
|
+
return err(e);
|
|
34435
|
+
}
|
|
34436
|
+
});
|
|
34437
|
+
server.tool("browser_extract_structured", "Extract structured data from page: tables, lists, JSON-LD, Open Graph, meta tags, and repeated elements (cards/items).", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
34438
|
+
try {
|
|
34439
|
+
const sid = resolveSessionId(session_id);
|
|
34440
|
+
const page = getSessionPage(sid);
|
|
34441
|
+
const { extractStructuredData: extractStructuredData2 } = await Promise.resolve().then(() => exports_structured_extract);
|
|
34442
|
+
const data = await extractStructuredData2(page);
|
|
34443
|
+
return json({
|
|
34444
|
+
tables: data.tables.length,
|
|
34445
|
+
lists: data.lists.length,
|
|
34446
|
+
json_ld: data.jsonLd.length,
|
|
34447
|
+
open_graph: Object.keys(data.openGraph).length,
|
|
34448
|
+
meta_tags: Object.keys(data.metaTags).length,
|
|
34449
|
+
repeated_elements: data.repeatedElements.length,
|
|
34450
|
+
data
|
|
34451
|
+
});
|
|
34452
|
+
} catch (e) {
|
|
34453
|
+
return err(e);
|
|
34454
|
+
}
|
|
34455
|
+
});
|
|
34456
|
+
server.tool("browser_dataset_save", "Save extracted data as a named dataset for later use", { name: exports_external.string(), data: exports_external.array(exports_external.record(exports_external.unknown())), source_url: exports_external.string().optional() }, async ({ name, data, source_url }) => {
|
|
34457
|
+
try {
|
|
34458
|
+
const { saveDataset: saveDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
|
|
34459
|
+
const dataset = saveDataset2({ name, rows: data, sourceUrl: source_url });
|
|
34460
|
+
return json({ id: dataset.id, name: dataset.name, row_count: dataset.row_count });
|
|
34461
|
+
} catch (e) {
|
|
34462
|
+
return err(e);
|
|
34463
|
+
}
|
|
34464
|
+
});
|
|
34465
|
+
server.tool("browser_dataset_list", "List all saved datasets", {}, async () => {
|
|
34466
|
+
try {
|
|
34467
|
+
const { listDatasets: listDatasets2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
|
|
34468
|
+
return json({ datasets: listDatasets2() });
|
|
34469
|
+
} catch (e) {
|
|
34470
|
+
return err(e);
|
|
34471
|
+
}
|
|
34472
|
+
});
|
|
34473
|
+
server.tool("browser_dataset_export", "Export a dataset as JSON or CSV file", { name: exports_external.string(), format: exports_external.enum(["json", "csv"]).optional().default("json") }, async ({ name, format }) => {
|
|
34474
|
+
try {
|
|
34475
|
+
const { exportDataset: exportDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
|
|
34476
|
+
return json(exportDataset2(name, format));
|
|
34477
|
+
} catch (e) {
|
|
34478
|
+
return err(e);
|
|
34479
|
+
}
|
|
34480
|
+
});
|
|
34481
|
+
server.tool("browser_dataset_delete", "Delete a saved dataset", { name: exports_external.string() }, async ({ name }) => {
|
|
34482
|
+
try {
|
|
34483
|
+
const { deleteDataset: deleteDataset2 } = await Promise.resolve().then(() => (init_datasets(), exports_datasets));
|
|
34484
|
+
return json({ deleted: deleteDataset2(name) });
|
|
34485
|
+
} catch (e) {
|
|
34486
|
+
return err(e);
|
|
34487
|
+
}
|
|
34488
|
+
});
|
|
32882
34489
|
server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
|
|
32883
34490
|
try {
|
|
32884
34491
|
const groups = {
|
|
@@ -32902,6 +34509,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32902
34509
|
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
32903
34510
|
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
32904
34511
|
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
34512
|
+
{ tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
|
|
32905
34513
|
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
32906
34514
|
],
|
|
32907
34515
|
Extraction: [
|
|
@@ -32930,7 +34538,10 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32930
34538
|
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
32931
34539
|
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
32932
34540
|
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
32933
|
-
{ tool: "browser_profile_delete", description: "Delete a saved profile" }
|
|
34541
|
+
{ tool: "browser_profile_delete", description: "Delete a saved profile" },
|
|
34542
|
+
{ tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
|
|
34543
|
+
{ tool: "browser_session_list_states", description: "List saved storage states" },
|
|
34544
|
+
{ tool: "browser_session_delete_state", description: "Delete a saved storage state" }
|
|
32934
34545
|
],
|
|
32935
34546
|
Network: [
|
|
32936
34547
|
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
@@ -32954,6 +34565,27 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
32954
34565
|
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
32955
34566
|
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
32956
34567
|
],
|
|
34568
|
+
Auth: [
|
|
34569
|
+
{ tool: "browser_auth_record", description: "Start recording a login flow" },
|
|
34570
|
+
{ tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
|
|
34571
|
+
{ tool: "browser_auth_replay", description: "Replay a saved auth flow" },
|
|
34572
|
+
{ tool: "browser_auth_list", description: "List all saved auth flows" },
|
|
34573
|
+
{ tool: "browser_auth_delete", description: "Delete a saved auth flow" }
|
|
34574
|
+
],
|
|
34575
|
+
Workflows: [
|
|
34576
|
+
{ tool: "browser_workflow_save", description: "Save a recording as a reusable workflow" },
|
|
34577
|
+
{ tool: "browser_workflow_list", description: "List all saved workflows" },
|
|
34578
|
+
{ tool: "browser_workflow_run", description: "Run a workflow with self-healing replay" },
|
|
34579
|
+
{ tool: "browser_workflow_delete", description: "Delete a saved workflow" }
|
|
34580
|
+
],
|
|
34581
|
+
Data: [
|
|
34582
|
+
{ tool: "browser_extract_structured", description: "Extract tables, lists, JSON-LD, Open Graph, meta tags, repeated elements" },
|
|
34583
|
+
{ tool: "browser_detect_apis", description: "Scan network traffic for JSON API endpoints" },
|
|
34584
|
+
{ tool: "browser_dataset_save", description: "Save extracted data as a named dataset" },
|
|
34585
|
+
{ tool: "browser_dataset_list", description: "List all saved datasets" },
|
|
34586
|
+
{ tool: "browser_dataset_export", description: "Export dataset as JSON or CSV" },
|
|
34587
|
+
{ tool: "browser_dataset_delete", description: "Delete a saved dataset" }
|
|
34588
|
+
],
|
|
32957
34589
|
Crawl: [
|
|
32958
34590
|
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
32959
34591
|
],
|
|
@@ -33007,10 +34639,13 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
33007
34639
|
{ tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
|
|
33008
34640
|
{ tool: "browser_version", description: "Show running binary version and tool count" },
|
|
33009
34641
|
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
34642
|
+
{ tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
|
|
34643
|
+
{ tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
|
|
33010
34644
|
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
33011
34645
|
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
33012
34646
|
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
33013
|
-
{ tool: "browser_watch_stop", description: "Stop DOM watcher" }
|
|
34647
|
+
{ tool: "browser_watch_stop", description: "Stop DOM watcher" },
|
|
34648
|
+
{ tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
|
|
33014
34649
|
]
|
|
33015
34650
|
};
|
|
33016
34651
|
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
@@ -33293,6 +34928,82 @@ server.tool("browser_batch", "Execute multiple browser actions in one call. Retu
|
|
|
33293
34928
|
return err(e);
|
|
33294
34929
|
}
|
|
33295
34930
|
});
|
|
34931
|
+
server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
|
|
34932
|
+
actions: exports_external.array(exports_external.object({
|
|
34933
|
+
session_id: exports_external.string().describe("Target session ID"),
|
|
34934
|
+
tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
|
|
34935
|
+
args: exports_external.record(exports_external.unknown()).optional().default({})
|
|
34936
|
+
})),
|
|
34937
|
+
timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
|
|
34938
|
+
}, async ({ actions, timeout }) => {
|
|
34939
|
+
try {
|
|
34940
|
+
const t0 = Date.now();
|
|
34941
|
+
const promises = actions.map(async (action, index) => {
|
|
34942
|
+
try {
|
|
34943
|
+
const sid = action.session_id;
|
|
34944
|
+
const page = getSessionPage(sid);
|
|
34945
|
+
const args = action.args;
|
|
34946
|
+
const toolName = action.tool.replace(/^browser_/, "");
|
|
34947
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
|
|
34948
|
+
const actionPromise = (async () => {
|
|
34949
|
+
switch (toolName) {
|
|
34950
|
+
case "navigate": {
|
|
34951
|
+
await navigate(page, args.url);
|
|
34952
|
+
const title = await page.title();
|
|
34953
|
+
return { url: page.url(), title };
|
|
34954
|
+
}
|
|
34955
|
+
case "screenshot": {
|
|
34956
|
+
const result2 = await takeScreenshot(page, {
|
|
34957
|
+
maxWidth: args.max_width ?? 800,
|
|
34958
|
+
quality: args.quality ?? 60
|
|
34959
|
+
});
|
|
34960
|
+
return { path: result2.path, size_bytes: result2.size_bytes };
|
|
34961
|
+
}
|
|
34962
|
+
case "click": {
|
|
34963
|
+
if (args.selector)
|
|
34964
|
+
await click(page, args.selector);
|
|
34965
|
+
return { clicked: args.selector };
|
|
34966
|
+
}
|
|
34967
|
+
case "type": {
|
|
34968
|
+
if (args.selector && args.text)
|
|
34969
|
+
await type(page, args.selector, args.text);
|
|
34970
|
+
return { typed: args.text };
|
|
34971
|
+
}
|
|
34972
|
+
case "get_text": {
|
|
34973
|
+
const text = await getText(page);
|
|
34974
|
+
return { text: text.slice(0, 1000), length: text.length };
|
|
34975
|
+
}
|
|
34976
|
+
case "get_links": {
|
|
34977
|
+
const links = await getLinks(page);
|
|
34978
|
+
return { links, count: links.length };
|
|
34979
|
+
}
|
|
34980
|
+
case "snapshot": {
|
|
34981
|
+
const snap = await takeSnapshot(page, sid);
|
|
34982
|
+
return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
|
|
34983
|
+
}
|
|
34984
|
+
case "evaluate": {
|
|
34985
|
+
const result2 = await page.evaluate(args.expression);
|
|
34986
|
+
return { result: result2 };
|
|
34987
|
+
}
|
|
34988
|
+
default:
|
|
34989
|
+
return { error: `Unknown tool: ${action.tool}` };
|
|
34990
|
+
}
|
|
34991
|
+
})();
|
|
34992
|
+
const result = await Promise.race([actionPromise, timeoutPromise]);
|
|
34993
|
+
return { index, session_id: sid, tool: action.tool, success: true, result };
|
|
34994
|
+
} catch (e) {
|
|
34995
|
+
return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
|
|
34996
|
+
}
|
|
34997
|
+
});
|
|
34998
|
+
const results = await Promise.all(promises);
|
|
34999
|
+
const duration_ms = Date.now() - t0;
|
|
35000
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
35001
|
+
const failed = results.filter((r) => !r.success).length;
|
|
35002
|
+
return json({ results, duration_ms, succeeded, failed, total: actions.length });
|
|
35003
|
+
} catch (e) {
|
|
35004
|
+
return err(e);
|
|
35005
|
+
}
|
|
35006
|
+
});
|
|
33296
35007
|
server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
|
|
33297
35008
|
try {
|
|
33298
35009
|
return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
|