@hasna/browser 0.0.9 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +1254 -349
- package/dist/engines/cdp.d.ts +2 -1
- package/dist/engines/cdp.d.ts.map +1 -1
- package/dist/index.js +497 -214
- package/dist/lib/actions.d.ts +22 -4
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/auth-flow.d.ts +43 -0
- package/dist/lib/auth-flow.d.ts.map +1 -0
- package/dist/lib/sanitize.d.ts +21 -0
- package/dist/lib/sanitize.d.ts.map +1 -0
- package/dist/lib/self-heal.d.ts +18 -0
- package/dist/lib/self-heal.d.ts.map +1 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/storage-state.d.ts +15 -0
- package/dist/lib/storage-state.d.ts.map +1 -0
- package/dist/lib/vision-fallback.d.ts +29 -0
- package/dist/lib/vision-fallback.d.ts.map +1 -0
- package/dist/mcp/index.js +1360 -470
- package/dist/server/index.js +374 -158
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2341,6 +2341,23 @@ function runMigrations(db) {
|
|
|
2341
2341
|
);
|
|
2342
2342
|
CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
|
|
2343
2343
|
`
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
version: 6,
|
|
2347
|
+
sql: `
|
|
2348
|
+
CREATE TABLE IF NOT EXISTS auth_flows (
|
|
2349
|
+
id TEXT PRIMARY KEY,
|
|
2350
|
+
name TEXT NOT NULL UNIQUE,
|
|
2351
|
+
domain TEXT NOT NULL,
|
|
2352
|
+
recording_id TEXT REFERENCES recordings(id),
|
|
2353
|
+
storage_state_path TEXT,
|
|
2354
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
2355
|
+
last_used TEXT
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
|
|
2359
|
+
CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
|
|
2360
|
+
`
|
|
2344
2361
|
}
|
|
2345
2362
|
];
|
|
2346
2363
|
for (const m of migrations) {
|
|
@@ -3456,6 +3473,188 @@ var init_dialogs = __esm(() => {
|
|
|
3456
3473
|
pendingDialogs = new Map;
|
|
3457
3474
|
});
|
|
3458
3475
|
|
|
3476
|
+
// src/engines/cdp.ts
|
|
3477
|
+
var exports_cdp = {};
|
|
3478
|
+
__export(exports_cdp, {
|
|
3479
|
+
connectToExistingBrowser: () => connectToExistingBrowser,
|
|
3480
|
+
CDPClient: () => CDPClient
|
|
3481
|
+
});
|
|
3482
|
+
async function connectToExistingBrowser(cdpUrl) {
|
|
3483
|
+
const { chromium: chromium3 } = await import("playwright");
|
|
3484
|
+
try {
|
|
3485
|
+
return await chromium3.connectOverCDP(cdpUrl);
|
|
3486
|
+
} catch (err) {
|
|
3487
|
+
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);
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
|
|
3491
|
+
class CDPClient {
|
|
3492
|
+
session;
|
|
3493
|
+
networkEnabled = false;
|
|
3494
|
+
performanceEnabled = false;
|
|
3495
|
+
constructor(session) {
|
|
3496
|
+
this.session = session;
|
|
3497
|
+
}
|
|
3498
|
+
static async fromPage(page) {
|
|
3499
|
+
try {
|
|
3500
|
+
const session = await page.context().newCDPSession(page);
|
|
3501
|
+
return new CDPClient(session);
|
|
3502
|
+
} catch (err) {
|
|
3503
|
+
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
async send(method, params) {
|
|
3507
|
+
try {
|
|
3508
|
+
return await this.session.send(method, params);
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
3511
|
+
}
|
|
3512
|
+
}
|
|
3513
|
+
on(event, handler) {
|
|
3514
|
+
this.session.on(event, handler);
|
|
3515
|
+
}
|
|
3516
|
+
off(event, handler) {
|
|
3517
|
+
this.session.off(event, handler);
|
|
3518
|
+
}
|
|
3519
|
+
async enableNetwork() {
|
|
3520
|
+
if (!this.networkEnabled) {
|
|
3521
|
+
await this.send("Network.enable");
|
|
3522
|
+
this.networkEnabled = true;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
async enablePerformance() {
|
|
3526
|
+
if (!this.performanceEnabled) {
|
|
3527
|
+
await this.send("Performance.enable");
|
|
3528
|
+
this.performanceEnabled = true;
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
async getPerformanceMetrics() {
|
|
3532
|
+
await this.enablePerformance();
|
|
3533
|
+
const result = await this.send("Performance.getMetrics");
|
|
3534
|
+
const m = {};
|
|
3535
|
+
for (const metric of result.metrics) {
|
|
3536
|
+
m[metric.name] = metric.value;
|
|
3537
|
+
}
|
|
3538
|
+
return {
|
|
3539
|
+
js_heap_size_used: m["JSHeapUsedSize"],
|
|
3540
|
+
js_heap_size_total: m["JSHeapTotalSize"],
|
|
3541
|
+
dom_interactive: m["DOMInteractive"],
|
|
3542
|
+
dom_complete: m["DOMComplete"],
|
|
3543
|
+
load_event: m["LoadEventEnd"]
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
async startJSCoverage() {
|
|
3547
|
+
await this.send("Profiler.enable");
|
|
3548
|
+
await this.send("Debugger.enable");
|
|
3549
|
+
await this.send("Profiler.startPreciseCoverage", {
|
|
3550
|
+
callCount: false,
|
|
3551
|
+
detailed: true
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
async stopJSCoverage() {
|
|
3555
|
+
const result = await this.send("Profiler.takePreciseCoverage");
|
|
3556
|
+
await this.send("Profiler.stopPreciseCoverage");
|
|
3557
|
+
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
3558
|
+
url: r.url,
|
|
3559
|
+
text: "",
|
|
3560
|
+
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
3561
|
+
}));
|
|
3562
|
+
}
|
|
3563
|
+
async getCoverage() {
|
|
3564
|
+
await this.startJSCoverage();
|
|
3565
|
+
const js = await this.stopJSCoverage();
|
|
3566
|
+
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
3567
|
+
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
3568
|
+
}
|
|
3569
|
+
async captureHAREntries(page, handler) {
|
|
3570
|
+
await this.enableNetwork();
|
|
3571
|
+
const requestTimings = new Map;
|
|
3572
|
+
const onRequest = (params) => {
|
|
3573
|
+
requestTimings.set(params.requestId, params.timestamp);
|
|
3574
|
+
};
|
|
3575
|
+
const onResponse = (params) => {
|
|
3576
|
+
const start = requestTimings.get(params.requestId);
|
|
3577
|
+
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
3578
|
+
handler({
|
|
3579
|
+
method: "GET",
|
|
3580
|
+
url: params.response.url,
|
|
3581
|
+
status: params.response.status,
|
|
3582
|
+
duration
|
|
3583
|
+
});
|
|
3584
|
+
};
|
|
3585
|
+
this.on("Network.requestWillBeSent", onRequest);
|
|
3586
|
+
this.on("Network.responseReceived", onResponse);
|
|
3587
|
+
return () => {
|
|
3588
|
+
this.off("Network.requestWillBeSent", onRequest);
|
|
3589
|
+
this.off("Network.responseReceived", onResponse);
|
|
3590
|
+
};
|
|
3591
|
+
}
|
|
3592
|
+
async detach() {
|
|
3593
|
+
try {
|
|
3594
|
+
await this.session.detach();
|
|
3595
|
+
} catch {}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
var init_cdp = __esm(() => {
|
|
3599
|
+
init_types();
|
|
3600
|
+
});
|
|
3601
|
+
|
|
3602
|
+
// src/lib/storage-state.ts
|
|
3603
|
+
var exports_storage_state = {};
|
|
3604
|
+
__export(exports_storage_state, {
|
|
3605
|
+
saveStateFromPage: () => saveStateFromPage,
|
|
3606
|
+
saveState: () => saveState,
|
|
3607
|
+
loadStatePath: () => loadStatePath,
|
|
3608
|
+
listStates: () => listStates,
|
|
3609
|
+
deleteState: () => deleteState
|
|
3610
|
+
});
|
|
3611
|
+
import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
|
|
3612
|
+
import { join as join3 } from "path";
|
|
3613
|
+
import { homedir as homedir3 } from "os";
|
|
3614
|
+
function ensureDir() {
|
|
3615
|
+
mkdirSync3(STATES_DIR, { recursive: true });
|
|
3616
|
+
}
|
|
3617
|
+
function statePath(name) {
|
|
3618
|
+
return join3(STATES_DIR, `${name}.json`);
|
|
3619
|
+
}
|
|
3620
|
+
async function saveState(context, name) {
|
|
3621
|
+
ensureDir();
|
|
3622
|
+
const path = statePath(name);
|
|
3623
|
+
const state = await context.storageState({ path });
|
|
3624
|
+
return path;
|
|
3625
|
+
}
|
|
3626
|
+
async function saveStateFromPage(page, name) {
|
|
3627
|
+
return saveState(page.context(), name);
|
|
3628
|
+
}
|
|
3629
|
+
function loadStatePath(name) {
|
|
3630
|
+
const path = statePath(name);
|
|
3631
|
+
return existsSync(path) ? path : null;
|
|
3632
|
+
}
|
|
3633
|
+
function listStates() {
|
|
3634
|
+
ensureDir();
|
|
3635
|
+
return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
|
|
3636
|
+
const path = join3(STATES_DIR, f);
|
|
3637
|
+
const stat = Bun.file(path);
|
|
3638
|
+
return {
|
|
3639
|
+
name: f.replace(".json", ""),
|
|
3640
|
+
path,
|
|
3641
|
+
modified: new Date(stat.lastModified).toISOString()
|
|
3642
|
+
};
|
|
3643
|
+
}).sort((a, b) => b.modified.localeCompare(a.modified));
|
|
3644
|
+
}
|
|
3645
|
+
function deleteState(name) {
|
|
3646
|
+
const path = statePath(name);
|
|
3647
|
+
if (existsSync(path)) {
|
|
3648
|
+
unlinkSync(path);
|
|
3649
|
+
return true;
|
|
3650
|
+
}
|
|
3651
|
+
return false;
|
|
3652
|
+
}
|
|
3653
|
+
var STATES_DIR;
|
|
3654
|
+
var init_storage_state = __esm(() => {
|
|
3655
|
+
STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
|
|
3656
|
+
});
|
|
3657
|
+
|
|
3459
3658
|
// src/lib/session.ts
|
|
3460
3659
|
var exports_session = {};
|
|
3461
3660
|
__export(exports_session, {
|
|
@@ -3485,6 +3684,37 @@ function createBunProxy(view) {
|
|
|
3485
3684
|
return view;
|
|
3486
3685
|
}
|
|
3487
3686
|
async function createSession2(opts = {}) {
|
|
3687
|
+
if (opts.cdpUrl) {
|
|
3688
|
+
const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
|
|
3689
|
+
const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
|
|
3690
|
+
const contexts = cdpBrowser.contexts();
|
|
3691
|
+
const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
|
|
3692
|
+
const pages = context.pages();
|
|
3693
|
+
const page2 = pages.length > 0 ? pages[0] : await context.newPage();
|
|
3694
|
+
const session2 = createSession({
|
|
3695
|
+
engine: "cdp",
|
|
3696
|
+
projectId: opts.projectId,
|
|
3697
|
+
agentId: opts.agentId,
|
|
3698
|
+
startUrl: page2.url(),
|
|
3699
|
+
name: opts.name ?? "attached"
|
|
3700
|
+
});
|
|
3701
|
+
const cleanups2 = [];
|
|
3702
|
+
if (opts.captureNetwork !== false) {
|
|
3703
|
+
try {
|
|
3704
|
+
cleanups2.push(enableNetworkLogging(page2, session2.id));
|
|
3705
|
+
} catch {}
|
|
3706
|
+
}
|
|
3707
|
+
if (opts.captureConsole !== false) {
|
|
3708
|
+
try {
|
|
3709
|
+
cleanups2.push(enableConsoleCapture(page2, session2.id));
|
|
3710
|
+
} catch {}
|
|
3711
|
+
}
|
|
3712
|
+
try {
|
|
3713
|
+
cleanups2.push(setupDialogHandler(page2, session2.id));
|
|
3714
|
+
} catch {}
|
|
3715
|
+
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 });
|
|
3716
|
+
return { session: session2, page: page2 };
|
|
3717
|
+
}
|
|
3488
3718
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
3489
3719
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
3490
3720
|
let browser = null;
|
|
@@ -3510,7 +3740,22 @@ async function createSession2(opts = {}) {
|
|
|
3510
3740
|
page = await context.newPage();
|
|
3511
3741
|
} else {
|
|
3512
3742
|
browser = await pool.acquire(opts.headless ?? true);
|
|
3513
|
-
|
|
3743
|
+
if (opts.storageState) {
|
|
3744
|
+
const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
3745
|
+
const statePath2 = loadStatePath2(opts.storageState);
|
|
3746
|
+
if (statePath2) {
|
|
3747
|
+
const context = await browser.newContext({
|
|
3748
|
+
viewport: opts.viewport ?? { width: 1280, height: 720 },
|
|
3749
|
+
userAgent: opts.userAgent,
|
|
3750
|
+
storageState: statePath2
|
|
3751
|
+
});
|
|
3752
|
+
page = await context.newPage();
|
|
3753
|
+
} else {
|
|
3754
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
3755
|
+
}
|
|
3756
|
+
} else {
|
|
3757
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
3758
|
+
}
|
|
3514
3759
|
}
|
|
3515
3760
|
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
3516
3761
|
try {
|
|
@@ -4015,6 +4260,66 @@ var init_snapshot = __esm(() => {
|
|
|
4015
4260
|
];
|
|
4016
4261
|
});
|
|
4017
4262
|
|
|
4263
|
+
// src/lib/self-heal.ts
|
|
4264
|
+
async function healSelector(page, selector, sessionId) {
|
|
4265
|
+
const attempts = [];
|
|
4266
|
+
attempts.push(`selector: ${selector}`);
|
|
4267
|
+
try {
|
|
4268
|
+
const loc = page.locator(selector).first();
|
|
4269
|
+
if (await loc.count() > 0) {
|
|
4270
|
+
return { found: true, locator: loc, method: "original", healed: false, attempts };
|
|
4271
|
+
}
|
|
4272
|
+
} catch {}
|
|
4273
|
+
if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
|
|
4274
|
+
attempts.push(`text: "${selector}"`);
|
|
4275
|
+
try {
|
|
4276
|
+
const loc = page.getByText(selector, { exact: false }).first();
|
|
4277
|
+
if (await loc.count() > 0) {
|
|
4278
|
+
return { found: true, locator: loc, method: "text", healed: true, attempts };
|
|
4279
|
+
}
|
|
4280
|
+
} catch {}
|
|
4281
|
+
}
|
|
4282
|
+
const roleMap = {
|
|
4283
|
+
button: ["button", "submit", "reset"],
|
|
4284
|
+
link: ["a"],
|
|
4285
|
+
input: ["input", "textarea"],
|
|
4286
|
+
heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
|
|
4287
|
+
};
|
|
4288
|
+
const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
|
|
4289
|
+
for (const [role, tags] of Object.entries(roleMap)) {
|
|
4290
|
+
attempts.push(`role: ${role} name~="${nameHint}"`);
|
|
4291
|
+
try {
|
|
4292
|
+
const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
|
|
4293
|
+
if (await loc.count() > 0) {
|
|
4294
|
+
return { found: true, locator: loc, method: "role", healed: true, attempts };
|
|
4295
|
+
}
|
|
4296
|
+
} catch {}
|
|
4297
|
+
}
|
|
4298
|
+
if (selector.startsWith("#")) {
|
|
4299
|
+
const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
4300
|
+
const partialSel = `[id*="${idPart}"]`;
|
|
4301
|
+
attempts.push(`partial_id: ${partialSel}`);
|
|
4302
|
+
try {
|
|
4303
|
+
const loc = page.locator(partialSel).first();
|
|
4304
|
+
if (await loc.count() > 0) {
|
|
4305
|
+
return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
|
|
4306
|
+
}
|
|
4307
|
+
} catch {}
|
|
4308
|
+
}
|
|
4309
|
+
if (selector.startsWith(".")) {
|
|
4310
|
+
const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
|
|
4311
|
+
const partialSel = `[class*="${classPart}"]`;
|
|
4312
|
+
attempts.push(`partial_class: ${partialSel}`);
|
|
4313
|
+
try {
|
|
4314
|
+
const loc = page.locator(partialSel).first();
|
|
4315
|
+
if (await loc.count() > 0) {
|
|
4316
|
+
return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
|
|
4317
|
+
}
|
|
4318
|
+
} catch {}
|
|
4319
|
+
}
|
|
4320
|
+
return { found: false, locator: null, method: "none", healed: false, attempts };
|
|
4321
|
+
}
|
|
4322
|
+
|
|
4018
4323
|
// src/lib/actions.ts
|
|
4019
4324
|
var exports_actions = {};
|
|
4020
4325
|
__export(exports_actions, {
|
|
@@ -4056,11 +4361,22 @@ async function click(page, selector, opts) {
|
|
|
4056
4361
|
delay: opts?.delay,
|
|
4057
4362
|
timeout: opts?.timeout ?? 1e4
|
|
4058
4363
|
});
|
|
4059
|
-
|
|
4060
|
-
|
|
4364
|
+
return {};
|
|
4365
|
+
} catch (originalError) {
|
|
4366
|
+
if (opts?.selfHeal !== false) {
|
|
4367
|
+
const result = await healSelector(page, selector);
|
|
4368
|
+
if (result.found && result.locator) {
|
|
4369
|
+
await result.locator.click({
|
|
4370
|
+
button: opts?.button ?? "left",
|
|
4371
|
+
timeout: opts?.timeout ?? 1e4
|
|
4372
|
+
});
|
|
4373
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
4374
|
+
}
|
|
4375
|
+
}
|
|
4376
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
4061
4377
|
throw new ElementNotFoundError(selector);
|
|
4062
4378
|
}
|
|
4063
|
-
throw new BrowserError(`Click failed on '${selector}': ${
|
|
4379
|
+
throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
|
|
4064
4380
|
}
|
|
4065
4381
|
}
|
|
4066
4382
|
async function type(page, selector, text, opts) {
|
|
@@ -4069,17 +4385,35 @@ async function type(page, selector, text, opts) {
|
|
|
4069
4385
|
await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
|
|
4070
4386
|
}
|
|
4071
4387
|
await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
4072
|
-
|
|
4073
|
-
|
|
4388
|
+
return {};
|
|
4389
|
+
} catch (originalError) {
|
|
4390
|
+
if (opts?.selfHeal !== false) {
|
|
4391
|
+
const result = await healSelector(page, selector);
|
|
4392
|
+
if (result.found && result.locator) {
|
|
4393
|
+
if (opts?.clear)
|
|
4394
|
+
await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
|
|
4395
|
+
await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
4396
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
4397
|
+
}
|
|
4398
|
+
}
|
|
4399
|
+
if (originalError instanceof Error && originalError.message.includes("not found")) {
|
|
4074
4400
|
throw new ElementNotFoundError(selector);
|
|
4075
4401
|
}
|
|
4076
|
-
throw new BrowserError(`Type failed on '${selector}': ${
|
|
4402
|
+
throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
|
|
4077
4403
|
}
|
|
4078
4404
|
}
|
|
4079
|
-
async function fill(page, selector, value, timeout = 1e4) {
|
|
4405
|
+
async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
|
|
4080
4406
|
try {
|
|
4081
4407
|
await page.fill(selector, value, { timeout });
|
|
4082
|
-
|
|
4408
|
+
return {};
|
|
4409
|
+
} catch (originalError) {
|
|
4410
|
+
if (selfHeal) {
|
|
4411
|
+
const result = await healSelector(page, selector);
|
|
4412
|
+
if (result.found && result.locator) {
|
|
4413
|
+
await result.locator.fill(value, { timeout });
|
|
4414
|
+
return { healed: true, method: result.method, attempts: result.attempts };
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4083
4417
|
throw new ElementNotFoundError(selector);
|
|
4084
4418
|
}
|
|
4085
4419
|
}
|
|
@@ -4204,12 +4538,39 @@ async function clickText(page, text, opts) {
|
|
|
4204
4538
|
}
|
|
4205
4539
|
}, { retries: opts?.retries ?? 1 });
|
|
4206
4540
|
}
|
|
4207
|
-
async function fillForm(page, fields, submitSelector) {
|
|
4541
|
+
async function fillForm(page, fields, submitSelector, selfHeal = true) {
|
|
4208
4542
|
let filled = 0;
|
|
4209
4543
|
const errors = [];
|
|
4544
|
+
const healedFields = [];
|
|
4210
4545
|
for (const [selector, value] of Object.entries(fields)) {
|
|
4211
4546
|
try {
|
|
4212
|
-
|
|
4547
|
+
let el = await page.$(selector);
|
|
4548
|
+
if (!el && selfHeal) {
|
|
4549
|
+
const result = await healSelector(page, selector);
|
|
4550
|
+
if (result.found && result.locator) {
|
|
4551
|
+
const handle = await result.locator.elementHandle();
|
|
4552
|
+
if (handle) {
|
|
4553
|
+
el = handle;
|
|
4554
|
+
healedFields.push(selector);
|
|
4555
|
+
const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
|
|
4556
|
+
const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
|
|
4557
|
+
if (tagName2 === "select") {
|
|
4558
|
+
await result.locator.selectOption(String(value));
|
|
4559
|
+
} else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
|
|
4560
|
+
if (Boolean(value))
|
|
4561
|
+
await result.locator.check();
|
|
4562
|
+
else
|
|
4563
|
+
await result.locator.uncheck();
|
|
4564
|
+
} else {
|
|
4565
|
+
await result.locator.fill(String(value));
|
|
4566
|
+
}
|
|
4567
|
+
filled++;
|
|
4568
|
+
continue;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
errors.push(`${selector}: element not found`);
|
|
4572
|
+
continue;
|
|
4573
|
+
}
|
|
4213
4574
|
if (!el) {
|
|
4214
4575
|
errors.push(`${selector}: element not found`);
|
|
4215
4576
|
continue;
|
|
@@ -4236,11 +4597,21 @@ async function fillForm(page, fields, submitSelector) {
|
|
|
4236
4597
|
if (submitSelector) {
|
|
4237
4598
|
try {
|
|
4238
4599
|
await page.click(submitSelector);
|
|
4239
|
-
} catch (
|
|
4240
|
-
|
|
4600
|
+
} catch (submitErr) {
|
|
4601
|
+
if (selfHeal) {
|
|
4602
|
+
const result = await healSelector(page, submitSelector);
|
|
4603
|
+
if (result.found && result.locator) {
|
|
4604
|
+
await result.locator.click();
|
|
4605
|
+
healedFields.push(submitSelector);
|
|
4606
|
+
} else {
|
|
4607
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
4608
|
+
}
|
|
4609
|
+
} else {
|
|
4610
|
+
errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
|
|
4611
|
+
}
|
|
4241
4612
|
}
|
|
4242
4613
|
}
|
|
4243
|
-
return { filled, errors, fields_attempted: Object.keys(fields).length };
|
|
4614
|
+
return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
|
|
4244
4615
|
}
|
|
4245
4616
|
async function waitForText(page, text, opts) {
|
|
4246
4617
|
const timeout = opts?.timeout ?? 1e4;
|
|
@@ -11096,17 +11467,17 @@ var init_gallery = __esm(() => {
|
|
|
11096
11467
|
});
|
|
11097
11468
|
|
|
11098
11469
|
// src/lib/screenshot.ts
|
|
11099
|
-
import { join as
|
|
11100
|
-
import { mkdirSync as
|
|
11101
|
-
import { homedir as
|
|
11470
|
+
import { join as join4 } from "path";
|
|
11471
|
+
import { mkdirSync as mkdirSync4 } from "fs";
|
|
11472
|
+
import { homedir as homedir4 } from "os";
|
|
11102
11473
|
function getDataDir2() {
|
|
11103
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
11474
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
11104
11475
|
}
|
|
11105
11476
|
function getScreenshotDir(projectId) {
|
|
11106
|
-
const base =
|
|
11477
|
+
const base = join4(getDataDir2(), "screenshots");
|
|
11107
11478
|
const date = new Date().toISOString().split("T")[0];
|
|
11108
|
-
const dir = projectId ?
|
|
11109
|
-
|
|
11479
|
+
const dir = projectId ? join4(base, projectId, date) : join4(base, date);
|
|
11480
|
+
mkdirSync4(dir, { recursive: true });
|
|
11110
11481
|
return dir;
|
|
11111
11482
|
}
|
|
11112
11483
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -11121,7 +11492,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
11121
11492
|
}
|
|
11122
11493
|
}
|
|
11123
11494
|
async function generateThumbnail(raw, dir, stem) {
|
|
11124
|
-
const thumbPath =
|
|
11495
|
+
const thumbPath = join4(dir, `${stem}.thumb.webp`);
|
|
11125
11496
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
11126
11497
|
await Bun.write(thumbPath, thumbBuffer);
|
|
11127
11498
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -11178,7 +11549,7 @@ async function takeScreenshot(page, opts) {
|
|
|
11178
11549
|
const compressedSizeBytes = finalBuffer.length;
|
|
11179
11550
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
11180
11551
|
const ext = format;
|
|
11181
|
-
const screenshotPath = opts?.path ??
|
|
11552
|
+
const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
|
|
11182
11553
|
await Bun.write(screenshotPath, finalBuffer);
|
|
11183
11554
|
let thumbnailPath;
|
|
11184
11555
|
let thumbnailBase64;
|
|
@@ -11238,12 +11609,12 @@ async function takeScreenshot(page, opts) {
|
|
|
11238
11609
|
}
|
|
11239
11610
|
async function generatePDF(page, opts) {
|
|
11240
11611
|
try {
|
|
11241
|
-
const base =
|
|
11612
|
+
const base = join4(getDataDir2(), "pdfs");
|
|
11242
11613
|
const date = new Date().toISOString().split("T")[0];
|
|
11243
|
-
const dir = opts?.projectId ?
|
|
11244
|
-
|
|
11614
|
+
const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
|
|
11615
|
+
mkdirSync4(dir, { recursive: true });
|
|
11245
11616
|
const timestamp = Date.now();
|
|
11246
|
-
const pdfPath = opts?.path ??
|
|
11617
|
+
const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
|
|
11247
11618
|
const buffer = await page.pdf({
|
|
11248
11619
|
path: pdfPath,
|
|
11249
11620
|
format: opts?.format ?? "A4",
|
|
@@ -11570,6 +11941,17 @@ var init_recordings = __esm(() => {
|
|
|
11570
11941
|
});
|
|
11571
11942
|
|
|
11572
11943
|
// src/lib/recorder.ts
|
|
11944
|
+
var exports_recorder = {};
|
|
11945
|
+
__export(exports_recorder, {
|
|
11946
|
+
stopRecording: () => stopRecording,
|
|
11947
|
+
startRecording: () => startRecording,
|
|
11948
|
+
replayRecording: () => replayRecording,
|
|
11949
|
+
recordStep: () => recordStep,
|
|
11950
|
+
listRecordings: () => listRecordings,
|
|
11951
|
+
getRecording: () => getRecording,
|
|
11952
|
+
exportRecording: () => exportRecording,
|
|
11953
|
+
attachPageListeners: () => attachPageListeners
|
|
11954
|
+
});
|
|
11573
11955
|
function startRecording(sessionId, name, startUrl) {
|
|
11574
11956
|
const steps = [];
|
|
11575
11957
|
const recording = createRecording({ name, start_url: startUrl, steps });
|
|
@@ -11580,6 +11962,23 @@ function startRecording(sessionId, name, startUrl) {
|
|
|
11580
11962
|
});
|
|
11581
11963
|
return recording;
|
|
11582
11964
|
}
|
|
11965
|
+
function attachPageListeners(page, recordingId) {
|
|
11966
|
+
const active = activeRecordings.get(recordingId);
|
|
11967
|
+
if (!active)
|
|
11968
|
+
throw new BrowserError(`No active recording: ${recordingId}`, "RECORDING_NOT_ACTIVE");
|
|
11969
|
+
const onFrameNav = () => {
|
|
11970
|
+
active.steps.push({
|
|
11971
|
+
type: "navigate",
|
|
11972
|
+
url: page.url(),
|
|
11973
|
+
timestamp: Date.now()
|
|
11974
|
+
});
|
|
11975
|
+
};
|
|
11976
|
+
page.on("framenavigated", onFrameNav);
|
|
11977
|
+
const cleanup = () => {
|
|
11978
|
+
page.off("framenavigated", onFrameNav);
|
|
11979
|
+
};
|
|
11980
|
+
active.cleanup = cleanup;
|
|
11981
|
+
}
|
|
11583
11982
|
function recordStep(recordingId, step) {
|
|
11584
11983
|
const active = activeRecordings.get(recordingId);
|
|
11585
11984
|
if (!active)
|
|
@@ -11651,6 +12050,64 @@ async function replayRecording(recordingId, page) {
|
|
|
11651
12050
|
duration_ms: Date.now() - startTime
|
|
11652
12051
|
};
|
|
11653
12052
|
}
|
|
12053
|
+
function exportRecording(recordingId, format = "json") {
|
|
12054
|
+
const recording = getRecording(recordingId);
|
|
12055
|
+
if (format === "json") {
|
|
12056
|
+
return JSON.stringify(recording, null, 2);
|
|
12057
|
+
}
|
|
12058
|
+
if (format === "playwright") {
|
|
12059
|
+
const lines2 = [
|
|
12060
|
+
`import { test, expect } from '@playwright/test';`,
|
|
12061
|
+
``,
|
|
12062
|
+
`test('${recording.name}', async ({ page }) => {`
|
|
12063
|
+
];
|
|
12064
|
+
for (const step of recording.steps) {
|
|
12065
|
+
switch (step.type) {
|
|
12066
|
+
case "navigate":
|
|
12067
|
+
lines2.push(` await page.goto('${step.url}');`);
|
|
12068
|
+
break;
|
|
12069
|
+
case "click":
|
|
12070
|
+
lines2.push(` await page.click('${step.selector}');`);
|
|
12071
|
+
break;
|
|
12072
|
+
case "type":
|
|
12073
|
+
lines2.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
12074
|
+
break;
|
|
12075
|
+
case "scroll":
|
|
12076
|
+
lines2.push(` await page.evaluate(() => window.scrollBy(0, 300));`);
|
|
12077
|
+
break;
|
|
12078
|
+
case "evaluate":
|
|
12079
|
+
lines2.push(` await page.evaluate(${step.value});`);
|
|
12080
|
+
break;
|
|
12081
|
+
}
|
|
12082
|
+
}
|
|
12083
|
+
lines2.push(`});`);
|
|
12084
|
+
return lines2.join(`
|
|
12085
|
+
`);
|
|
12086
|
+
}
|
|
12087
|
+
const lines = [
|
|
12088
|
+
`const puppeteer = require('puppeteer');`,
|
|
12089
|
+
``,
|
|
12090
|
+
`(async () => {`,
|
|
12091
|
+
` const browser = await puppeteer.launch();`,
|
|
12092
|
+
` const page = await browser.newPage();`
|
|
12093
|
+
];
|
|
12094
|
+
for (const step of recording.steps) {
|
|
12095
|
+
switch (step.type) {
|
|
12096
|
+
case "navigate":
|
|
12097
|
+
lines.push(` await page.goto('${step.url}');`);
|
|
12098
|
+
break;
|
|
12099
|
+
case "click":
|
|
12100
|
+
lines.push(` await page.click('${step.selector}');`);
|
|
12101
|
+
break;
|
|
12102
|
+
case "type":
|
|
12103
|
+
lines.push(` await page.type('${step.selector}', '${step.value}');`);
|
|
12104
|
+
break;
|
|
12105
|
+
}
|
|
12106
|
+
}
|
|
12107
|
+
lines.push(` await browser.close();`, `})();`);
|
|
12108
|
+
return lines.join(`
|
|
12109
|
+
`);
|
|
12110
|
+
}
|
|
11654
12111
|
var activeRecordings;
|
|
11655
12112
|
var init_recorder = __esm(() => {
|
|
11656
12113
|
init_recordings();
|
|
@@ -15625,118 +16082,6 @@ var init_zod = __esm(() => {
|
|
|
15625
16082
|
init_external();
|
|
15626
16083
|
});
|
|
15627
16084
|
|
|
15628
|
-
// src/engines/cdp.ts
|
|
15629
|
-
class CDPClient {
|
|
15630
|
-
session;
|
|
15631
|
-
networkEnabled = false;
|
|
15632
|
-
performanceEnabled = false;
|
|
15633
|
-
constructor(session) {
|
|
15634
|
-
this.session = session;
|
|
15635
|
-
}
|
|
15636
|
-
static async fromPage(page) {
|
|
15637
|
-
try {
|
|
15638
|
-
const session = await page.context().newCDPSession(page);
|
|
15639
|
-
return new CDPClient(session);
|
|
15640
|
-
} catch (err) {
|
|
15641
|
-
throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
|
|
15642
|
-
}
|
|
15643
|
-
}
|
|
15644
|
-
async send(method, params) {
|
|
15645
|
-
try {
|
|
15646
|
-
return await this.session.send(method, params);
|
|
15647
|
-
} catch (err) {
|
|
15648
|
-
throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
|
|
15649
|
-
}
|
|
15650
|
-
}
|
|
15651
|
-
on(event, handler) {
|
|
15652
|
-
this.session.on(event, handler);
|
|
15653
|
-
}
|
|
15654
|
-
off(event, handler) {
|
|
15655
|
-
this.session.off(event, handler);
|
|
15656
|
-
}
|
|
15657
|
-
async enableNetwork() {
|
|
15658
|
-
if (!this.networkEnabled) {
|
|
15659
|
-
await this.send("Network.enable");
|
|
15660
|
-
this.networkEnabled = true;
|
|
15661
|
-
}
|
|
15662
|
-
}
|
|
15663
|
-
async enablePerformance() {
|
|
15664
|
-
if (!this.performanceEnabled) {
|
|
15665
|
-
await this.send("Performance.enable");
|
|
15666
|
-
this.performanceEnabled = true;
|
|
15667
|
-
}
|
|
15668
|
-
}
|
|
15669
|
-
async getPerformanceMetrics() {
|
|
15670
|
-
await this.enablePerformance();
|
|
15671
|
-
const result = await this.send("Performance.getMetrics");
|
|
15672
|
-
const m = {};
|
|
15673
|
-
for (const metric of result.metrics) {
|
|
15674
|
-
m[metric.name] = metric.value;
|
|
15675
|
-
}
|
|
15676
|
-
return {
|
|
15677
|
-
js_heap_size_used: m["JSHeapUsedSize"],
|
|
15678
|
-
js_heap_size_total: m["JSHeapTotalSize"],
|
|
15679
|
-
dom_interactive: m["DOMInteractive"],
|
|
15680
|
-
dom_complete: m["DOMComplete"],
|
|
15681
|
-
load_event: m["LoadEventEnd"]
|
|
15682
|
-
};
|
|
15683
|
-
}
|
|
15684
|
-
async startJSCoverage() {
|
|
15685
|
-
await this.send("Profiler.enable");
|
|
15686
|
-
await this.send("Debugger.enable");
|
|
15687
|
-
await this.send("Profiler.startPreciseCoverage", {
|
|
15688
|
-
callCount: false,
|
|
15689
|
-
detailed: true
|
|
15690
|
-
});
|
|
15691
|
-
}
|
|
15692
|
-
async stopJSCoverage() {
|
|
15693
|
-
const result = await this.send("Profiler.takePreciseCoverage");
|
|
15694
|
-
await this.send("Profiler.stopPreciseCoverage");
|
|
15695
|
-
return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
|
|
15696
|
-
url: r.url,
|
|
15697
|
-
text: "",
|
|
15698
|
-
ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
|
|
15699
|
-
}));
|
|
15700
|
-
}
|
|
15701
|
-
async getCoverage() {
|
|
15702
|
-
await this.startJSCoverage();
|
|
15703
|
-
const js = await this.stopJSCoverage();
|
|
15704
|
-
const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
|
|
15705
|
-
return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
|
|
15706
|
-
}
|
|
15707
|
-
async captureHAREntries(page, handler) {
|
|
15708
|
-
await this.enableNetwork();
|
|
15709
|
-
const requestTimings = new Map;
|
|
15710
|
-
const onRequest = (params) => {
|
|
15711
|
-
requestTimings.set(params.requestId, params.timestamp);
|
|
15712
|
-
};
|
|
15713
|
-
const onResponse = (params) => {
|
|
15714
|
-
const start = requestTimings.get(params.requestId);
|
|
15715
|
-
const duration = start != null ? (params.timestamp - start) * 1000 : 0;
|
|
15716
|
-
handler({
|
|
15717
|
-
method: "GET",
|
|
15718
|
-
url: params.response.url,
|
|
15719
|
-
status: params.response.status,
|
|
15720
|
-
duration
|
|
15721
|
-
});
|
|
15722
|
-
};
|
|
15723
|
-
this.on("Network.requestWillBeSent", onRequest);
|
|
15724
|
-
this.on("Network.responseReceived", onResponse);
|
|
15725
|
-
return () => {
|
|
15726
|
-
this.off("Network.requestWillBeSent", onRequest);
|
|
15727
|
-
this.off("Network.responseReceived", onResponse);
|
|
15728
|
-
};
|
|
15729
|
-
}
|
|
15730
|
-
async detach() {
|
|
15731
|
-
try {
|
|
15732
|
-
await this.session.detach();
|
|
15733
|
-
} catch {}
|
|
15734
|
-
}
|
|
15735
|
-
}
|
|
15736
|
-
var init_cdp = __esm(() => {
|
|
15737
|
-
init_types();
|
|
15738
|
-
});
|
|
15739
|
-
|
|
15740
16085
|
// src/lib/performance.ts
|
|
15741
16086
|
async function getPerformanceMetrics(page) {
|
|
15742
16087
|
const navTiming = await page.evaluate(() => {
|
|
@@ -15842,16 +16187,16 @@ __export(exports_downloads, {
|
|
|
15842
16187
|
cleanStaleDownloads: () => cleanStaleDownloads
|
|
15843
16188
|
});
|
|
15844
16189
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
15845
|
-
import { join as
|
|
15846
|
-
import { mkdirSync as
|
|
15847
|
-
import { homedir as
|
|
16190
|
+
import { join as join5, basename, extname } from "path";
|
|
16191
|
+
import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
16192
|
+
import { homedir as homedir5 } from "os";
|
|
15848
16193
|
function getDataDir3() {
|
|
15849
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
16194
|
+
return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
15850
16195
|
}
|
|
15851
16196
|
function getDownloadsDir(sessionId) {
|
|
15852
|
-
const base =
|
|
15853
|
-
const dir = sessionId ?
|
|
15854
|
-
|
|
16197
|
+
const base = join5(getDataDir3(), "downloads");
|
|
16198
|
+
const dir = sessionId ? join5(base, sessionId) : base;
|
|
16199
|
+
mkdirSync5(dir, { recursive: true });
|
|
15855
16200
|
return dir;
|
|
15856
16201
|
}
|
|
15857
16202
|
function ensureDownloadsDir() {
|
|
@@ -15866,7 +16211,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
15866
16211
|
const ext = extname(filename) || "";
|
|
15867
16212
|
const stem = basename(filename, ext);
|
|
15868
16213
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
15869
|
-
const filePath =
|
|
16214
|
+
const filePath = join5(dir, uniqueName);
|
|
15870
16215
|
writeFileSync(filePath, buffer);
|
|
15871
16216
|
const meta = {
|
|
15872
16217
|
id,
|
|
@@ -15895,20 +16240,20 @@ function listDownloads(sessionId) {
|
|
|
15895
16240
|
const dir = getDownloadsDir(sessionId);
|
|
15896
16241
|
const results = [];
|
|
15897
16242
|
function scanDir(d) {
|
|
15898
|
-
if (!
|
|
16243
|
+
if (!existsSync2(d))
|
|
15899
16244
|
return;
|
|
15900
|
-
const entries =
|
|
16245
|
+
const entries = readdirSync2(d);
|
|
15901
16246
|
for (const entry of entries) {
|
|
15902
16247
|
if (entry.endsWith(".meta.json"))
|
|
15903
16248
|
continue;
|
|
15904
|
-
const full =
|
|
16249
|
+
const full = join5(d, entry);
|
|
15905
16250
|
const stat = statSync(full);
|
|
15906
16251
|
if (stat.isDirectory()) {
|
|
15907
16252
|
scanDir(full);
|
|
15908
16253
|
continue;
|
|
15909
16254
|
}
|
|
15910
16255
|
const mpath = metaPath(full);
|
|
15911
|
-
if (!
|
|
16256
|
+
if (!existsSync2(mpath))
|
|
15912
16257
|
continue;
|
|
15913
16258
|
try {
|
|
15914
16259
|
const meta = JSON.parse(readFileSync(mpath, "utf8"));
|
|
@@ -15938,9 +16283,9 @@ function deleteDownload(id, sessionId) {
|
|
|
15938
16283
|
if (!file)
|
|
15939
16284
|
return false;
|
|
15940
16285
|
try {
|
|
15941
|
-
|
|
15942
|
-
if (
|
|
15943
|
-
|
|
16286
|
+
unlinkSync2(file.path);
|
|
16287
|
+
if (existsSync2(file.meta_path))
|
|
16288
|
+
unlinkSync2(file.meta_path);
|
|
15944
16289
|
return true;
|
|
15945
16290
|
} catch {
|
|
15946
16291
|
return false;
|
|
@@ -15990,9 +16335,9 @@ var exports_gallery_diff = {};
|
|
|
15990
16335
|
__export(exports_gallery_diff, {
|
|
15991
16336
|
diffImages: () => diffImages
|
|
15992
16337
|
});
|
|
15993
|
-
import { join as
|
|
15994
|
-
import { mkdirSync as
|
|
15995
|
-
import { homedir as
|
|
16338
|
+
import { join as join6 } from "path";
|
|
16339
|
+
import { mkdirSync as mkdirSync6 } from "fs";
|
|
16340
|
+
import { homedir as homedir6 } from "os";
|
|
15996
16341
|
async function diffImages(path1, path2) {
|
|
15997
16342
|
const img1 = import_sharp2.default(path1);
|
|
15998
16343
|
const img2 = import_sharp2.default(path2);
|
|
@@ -16023,10 +16368,10 @@ async function diffImages(path1, path2) {
|
|
|
16023
16368
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
16024
16369
|
}
|
|
16025
16370
|
}
|
|
16026
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
16027
|
-
const diffDir =
|
|
16028
|
-
|
|
16029
|
-
const diffPath =
|
|
16371
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
16372
|
+
const diffDir = join6(dataDir, "diffs");
|
|
16373
|
+
mkdirSync6(diffDir, { recursive: true });
|
|
16374
|
+
const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
|
|
16030
16375
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
16031
16376
|
await Bun.write(diffPath, diffImageBuffer);
|
|
16032
16377
|
return {
|
|
@@ -16043,9 +16388,9 @@ var init_gallery_diff = __esm(() => {
|
|
|
16043
16388
|
});
|
|
16044
16389
|
|
|
16045
16390
|
// src/lib/files-integration.ts
|
|
16046
|
-
import { join as
|
|
16047
|
-
import { mkdirSync as
|
|
16048
|
-
import { homedir as
|
|
16391
|
+
import { join as join7 } from "path";
|
|
16392
|
+
import { mkdirSync as mkdirSync7, copyFileSync as copyFileSync2 } from "fs";
|
|
16393
|
+
import { homedir as homedir7 } from "os";
|
|
16049
16394
|
async function persistFile(localPath, opts) {
|
|
16050
16395
|
try {
|
|
16051
16396
|
const mod = await import("@hasna/files");
|
|
@@ -16054,12 +16399,12 @@ async function persistFile(localPath, opts) {
|
|
|
16054
16399
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
16055
16400
|
}
|
|
16056
16401
|
} catch {}
|
|
16057
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
16402
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
16058
16403
|
const date = new Date().toISOString().split("T")[0];
|
|
16059
|
-
const dir =
|
|
16060
|
-
|
|
16404
|
+
const dir = join7(dataDir, "persistent", date);
|
|
16405
|
+
mkdirSync7(dir, { recursive: true });
|
|
16061
16406
|
const filename = localPath.split("/").pop() ?? "file";
|
|
16062
|
-
const targetPath =
|
|
16407
|
+
const targetPath = join7(dir, filename);
|
|
16063
16408
|
copyFileSync2(localPath, targetPath);
|
|
16064
16409
|
return {
|
|
16065
16410
|
id: `local-${Date.now()}`,
|
|
@@ -16174,23 +16519,23 @@ __export(exports_profiles, {
|
|
|
16174
16519
|
deleteProfile: () => deleteProfile,
|
|
16175
16520
|
applyProfile: () => applyProfile
|
|
16176
16521
|
});
|
|
16177
|
-
import { mkdirSync as
|
|
16178
|
-
import { join as
|
|
16179
|
-
import { homedir as
|
|
16522
|
+
import { mkdirSync as mkdirSync8, existsSync as existsSync4, readdirSync as readdirSync3, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
16523
|
+
import { join as join8 } from "path";
|
|
16524
|
+
import { homedir as homedir8 } from "os";
|
|
16180
16525
|
function getProfilesDir() {
|
|
16181
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
16182
|
-
const dir =
|
|
16183
|
-
|
|
16526
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join8(homedir8(), ".browser");
|
|
16527
|
+
const dir = join8(dataDir, "profiles");
|
|
16528
|
+
mkdirSync8(dir, { recursive: true });
|
|
16184
16529
|
return dir;
|
|
16185
16530
|
}
|
|
16186
16531
|
function getProfileDir2(name) {
|
|
16187
|
-
return
|
|
16532
|
+
return join8(getProfilesDir(), name);
|
|
16188
16533
|
}
|
|
16189
16534
|
async function saveProfile(page, name) {
|
|
16190
16535
|
const dir = getProfileDir2(name);
|
|
16191
|
-
|
|
16536
|
+
mkdirSync8(dir, { recursive: true });
|
|
16192
16537
|
const cookies = await page.context().cookies();
|
|
16193
|
-
writeFileSync2(
|
|
16538
|
+
writeFileSync2(join8(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
16194
16539
|
let localStorage2 = {};
|
|
16195
16540
|
try {
|
|
16196
16541
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -16202,11 +16547,11 @@ async function saveProfile(page, name) {
|
|
|
16202
16547
|
return result;
|
|
16203
16548
|
});
|
|
16204
16549
|
} catch {}
|
|
16205
|
-
writeFileSync2(
|
|
16550
|
+
writeFileSync2(join8(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
16206
16551
|
const savedAt = new Date().toISOString();
|
|
16207
16552
|
const url = page.url();
|
|
16208
16553
|
const meta = { saved_at: savedAt, url };
|
|
16209
|
-
writeFileSync2(
|
|
16554
|
+
writeFileSync2(join8(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
16210
16555
|
return {
|
|
16211
16556
|
name,
|
|
16212
16557
|
saved_at: savedAt,
|
|
@@ -16217,17 +16562,17 @@ async function saveProfile(page, name) {
|
|
|
16217
16562
|
}
|
|
16218
16563
|
function loadProfile(name) {
|
|
16219
16564
|
const dir = getProfileDir2(name);
|
|
16220
|
-
if (!
|
|
16565
|
+
if (!existsSync4(dir)) {
|
|
16221
16566
|
throw new Error(`Profile not found: ${name}`);
|
|
16222
16567
|
}
|
|
16223
|
-
const cookiesPath =
|
|
16224
|
-
const storagePath =
|
|
16225
|
-
const metaPath2 =
|
|
16226
|
-
const cookies =
|
|
16227
|
-
const localStorage2 =
|
|
16568
|
+
const cookiesPath = join8(dir, "cookies.json");
|
|
16569
|
+
const storagePath = join8(dir, "storage.json");
|
|
16570
|
+
const metaPath2 = join8(dir, "meta.json");
|
|
16571
|
+
const cookies = existsSync4(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
16572
|
+
const localStorage2 = existsSync4(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
16228
16573
|
let savedAt = new Date().toISOString();
|
|
16229
16574
|
let url;
|
|
16230
|
-
if (
|
|
16575
|
+
if (existsSync4(metaPath2)) {
|
|
16231
16576
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
16232
16577
|
savedAt = meta.saved_at ?? savedAt;
|
|
16233
16578
|
url = meta.url;
|
|
@@ -16255,33 +16600,33 @@ async function applyProfile(page, profileData) {
|
|
|
16255
16600
|
}
|
|
16256
16601
|
function listProfiles() {
|
|
16257
16602
|
const dir = getProfilesDir();
|
|
16258
|
-
if (!
|
|
16603
|
+
if (!existsSync4(dir))
|
|
16259
16604
|
return [];
|
|
16260
|
-
const entries =
|
|
16605
|
+
const entries = readdirSync3(dir, { withFileTypes: true });
|
|
16261
16606
|
const profiles = [];
|
|
16262
16607
|
for (const entry of entries) {
|
|
16263
16608
|
if (!entry.isDirectory())
|
|
16264
16609
|
continue;
|
|
16265
16610
|
const name = entry.name;
|
|
16266
|
-
const profileDir =
|
|
16611
|
+
const profileDir = join8(dir, name);
|
|
16267
16612
|
let savedAt = "";
|
|
16268
16613
|
let url;
|
|
16269
16614
|
let cookieCount = 0;
|
|
16270
16615
|
let storageKeyCount = 0;
|
|
16271
16616
|
try {
|
|
16272
|
-
const metaPath2 =
|
|
16273
|
-
if (
|
|
16617
|
+
const metaPath2 = join8(profileDir, "meta.json");
|
|
16618
|
+
if (existsSync4(metaPath2)) {
|
|
16274
16619
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
16275
16620
|
savedAt = meta.saved_at ?? "";
|
|
16276
16621
|
url = meta.url;
|
|
16277
16622
|
}
|
|
16278
|
-
const cookiesPath =
|
|
16279
|
-
if (
|
|
16623
|
+
const cookiesPath = join8(profileDir, "cookies.json");
|
|
16624
|
+
if (existsSync4(cookiesPath)) {
|
|
16280
16625
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
16281
16626
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
16282
16627
|
}
|
|
16283
|
-
const storagePath =
|
|
16284
|
-
if (
|
|
16628
|
+
const storagePath = join8(profileDir, "storage.json");
|
|
16629
|
+
if (existsSync4(storagePath)) {
|
|
16285
16630
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
16286
16631
|
storageKeyCount = Object.keys(storage).length;
|
|
16287
16632
|
}
|
|
@@ -16298,7 +16643,7 @@ function listProfiles() {
|
|
|
16298
16643
|
}
|
|
16299
16644
|
function deleteProfile(name) {
|
|
16300
16645
|
const dir = getProfileDir2(name);
|
|
16301
|
-
if (!
|
|
16646
|
+
if (!existsSync4(dir))
|
|
16302
16647
|
return false;
|
|
16303
16648
|
try {
|
|
16304
16649
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -16309,6 +16654,97 @@ function deleteProfile(name) {
|
|
|
16309
16654
|
}
|
|
16310
16655
|
var init_profiles = () => {};
|
|
16311
16656
|
|
|
16657
|
+
// src/lib/sanitize.ts
|
|
16658
|
+
var exports_sanitize = {};
|
|
16659
|
+
__export(exports_sanitize, {
|
|
16660
|
+
sanitizeText: () => sanitizeText,
|
|
16661
|
+
sanitizeHTML: () => sanitizeHTML
|
|
16662
|
+
});
|
|
16663
|
+
function sanitizeText(text) {
|
|
16664
|
+
let stripped = 0;
|
|
16665
|
+
const warnings = [];
|
|
16666
|
+
let clean = text;
|
|
16667
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
16668
|
+
pattern.lastIndex = 0;
|
|
16669
|
+
const matches = clean.match(pattern);
|
|
16670
|
+
if (matches) {
|
|
16671
|
+
stripped += matches.length;
|
|
16672
|
+
warnings.push(`Stripped ${matches.length}x: ${pattern.source}`);
|
|
16673
|
+
pattern.lastIndex = 0;
|
|
16674
|
+
clean = clean.replace(pattern, "[STRIPPED]");
|
|
16675
|
+
}
|
|
16676
|
+
}
|
|
16677
|
+
return { text: clean, stripped, warnings };
|
|
16678
|
+
}
|
|
16679
|
+
function sanitizeHTML(html) {
|
|
16680
|
+
let stripped = 0;
|
|
16681
|
+
const warnings = [];
|
|
16682
|
+
let clean = html;
|
|
16683
|
+
const commentMatches = clean.match(/<!--[\s\S]*?-->/g);
|
|
16684
|
+
if (commentMatches) {
|
|
16685
|
+
for (const comment of commentMatches) {
|
|
16686
|
+
if (comment.replace(/<!--\s*-->/g, "").trim().length > 20) {
|
|
16687
|
+
stripped++;
|
|
16688
|
+
warnings.push(`Stripped HTML comment (${comment.length} chars)`);
|
|
16689
|
+
}
|
|
16690
|
+
}
|
|
16691
|
+
clean = clean.replace(/<!--[\s\S]*?-->/g, "");
|
|
16692
|
+
}
|
|
16693
|
+
const hiddenPatterns = [
|
|
16694
|
+
/style\s*=\s*"[^"]*display\s*:\s*none[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
16695
|
+
/style\s*=\s*"[^"]*visibility\s*:\s*hidden[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
16696
|
+
/style\s*=\s*"[^"]*opacity\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
16697
|
+
/style\s*=\s*"[^"]*font-size\s*:\s*0[^"]*"[^>]*>[\s\S]*?<\//gi,
|
|
16698
|
+
/style\s*=\s*"[^"]*position\s*:\s*absolute[^"]*left\s*:\s*-\d{4,}[^"]*"[^>]*>[\s\S]*?<\//gi
|
|
16699
|
+
];
|
|
16700
|
+
for (const pattern of hiddenPatterns) {
|
|
16701
|
+
pattern.lastIndex = 0;
|
|
16702
|
+
const matches = clean.match(pattern);
|
|
16703
|
+
if (matches) {
|
|
16704
|
+
stripped += matches.length;
|
|
16705
|
+
warnings.push(`Stripped ${matches.length} hidden elements`);
|
|
16706
|
+
pattern.lastIndex = 0;
|
|
16707
|
+
clean = clean.replace(pattern, "");
|
|
16708
|
+
}
|
|
16709
|
+
}
|
|
16710
|
+
const ariaHiddenPattern = /aria-hidden\s*=\s*"true"[^>]*>[\s\S]*?<\//gi;
|
|
16711
|
+
const ariaHidden = clean.match(ariaHiddenPattern);
|
|
16712
|
+
if (ariaHidden) {
|
|
16713
|
+
stripped += ariaHidden.length;
|
|
16714
|
+
warnings.push(`Stripped ${ariaHidden.length} aria-hidden elements`);
|
|
16715
|
+
ariaHiddenPattern.lastIndex = 0;
|
|
16716
|
+
clean = clean.replace(ariaHiddenPattern, "");
|
|
16717
|
+
}
|
|
16718
|
+
const textResult = sanitizeText(clean);
|
|
16719
|
+
return {
|
|
16720
|
+
text: textResult.text,
|
|
16721
|
+
stripped: stripped + textResult.stripped,
|
|
16722
|
+
warnings: [...warnings, ...textResult.warnings]
|
|
16723
|
+
};
|
|
16724
|
+
}
|
|
16725
|
+
var INJECTION_PATTERNS;
|
|
16726
|
+
var init_sanitize = __esm(() => {
|
|
16727
|
+
INJECTION_PATTERNS = [
|
|
16728
|
+
/ignore\s+(all\s+)?previous\s+instructions/gi,
|
|
16729
|
+
/ignore\s+(all\s+)?prior\s+instructions/gi,
|
|
16730
|
+
/disregard\s+(all\s+)?previous/gi,
|
|
16731
|
+
/forget\s+(all\s+)?previous/gi,
|
|
16732
|
+
/you\s+are\s+now\s+/gi,
|
|
16733
|
+
/new\s+instructions?\s*:/gi,
|
|
16734
|
+
/system\s+prompt\s*:/gi,
|
|
16735
|
+
/\[INST\]/gi,
|
|
16736
|
+
/\[\/INST\]/gi,
|
|
16737
|
+
/<\|im_start\|>/gi,
|
|
16738
|
+
/<\|im_end\|>/gi,
|
|
16739
|
+
/<<SYS>>/gi,
|
|
16740
|
+
/<<\/SYS>>/gi,
|
|
16741
|
+
/IMPORTANT:\s*ignore/gi,
|
|
16742
|
+
/CRITICAL:\s*override/gi,
|
|
16743
|
+
/assistant:\s/gi,
|
|
16744
|
+
/human:\s/gi
|
|
16745
|
+
];
|
|
16746
|
+
});
|
|
16747
|
+
|
|
16312
16748
|
// src/lib/annotate.ts
|
|
16313
16749
|
var exports_annotate = {};
|
|
16314
16750
|
__export(exports_annotate, {
|
|
@@ -16369,26 +16805,213 @@ var init_annotate = __esm(() => {
|
|
|
16369
16805
|
import_sharp3 = __toESM(require_lib(), 1);
|
|
16370
16806
|
});
|
|
16371
16807
|
|
|
16808
|
+
// src/lib/auth-flow.ts
|
|
16809
|
+
var exports_auth_flow = {};
|
|
16810
|
+
__export(exports_auth_flow, {
|
|
16811
|
+
tryReplayAuth: () => tryReplayAuth,
|
|
16812
|
+
touchAuthFlow: () => touchAuthFlow,
|
|
16813
|
+
saveAuthFlow: () => saveAuthFlow,
|
|
16814
|
+
listAuthFlows: () => listAuthFlows,
|
|
16815
|
+
isAuthRedirect: () => isAuthRedirect,
|
|
16816
|
+
isAuthPage: () => isAuthPage,
|
|
16817
|
+
getAuthFlowByName: () => getAuthFlowByName,
|
|
16818
|
+
getAuthFlowByDomain: () => getAuthFlowByDomain,
|
|
16819
|
+
getAuthFlow: () => getAuthFlow,
|
|
16820
|
+
deleteAuthFlow: () => deleteAuthFlow
|
|
16821
|
+
});
|
|
16822
|
+
import { randomUUID as randomUUID10 } from "crypto";
|
|
16823
|
+
function saveAuthFlow(data) {
|
|
16824
|
+
const db = getDatabase();
|
|
16825
|
+
const id = randomUUID10();
|
|
16826
|
+
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);
|
|
16827
|
+
return getAuthFlow(id);
|
|
16828
|
+
}
|
|
16829
|
+
function getAuthFlow(id) {
|
|
16830
|
+
const db = getDatabase();
|
|
16831
|
+
return db.query("SELECT * FROM auth_flows WHERE id = ?").get(id) ?? null;
|
|
16832
|
+
}
|
|
16833
|
+
function getAuthFlowByName(name) {
|
|
16834
|
+
const db = getDatabase();
|
|
16835
|
+
return db.query("SELECT * FROM auth_flows WHERE name = ?").get(name) ?? null;
|
|
16836
|
+
}
|
|
16837
|
+
function getAuthFlowByDomain(domain) {
|
|
16838
|
+
const db = getDatabase();
|
|
16839
|
+
return db.query("SELECT * FROM auth_flows WHERE domain = ? ORDER BY last_used DESC LIMIT 1").get(domain) ?? null;
|
|
16840
|
+
}
|
|
16841
|
+
function listAuthFlows() {
|
|
16842
|
+
const db = getDatabase();
|
|
16843
|
+
return db.query("SELECT * FROM auth_flows ORDER BY last_used DESC, created_at DESC").all();
|
|
16844
|
+
}
|
|
16845
|
+
function deleteAuthFlow(name) {
|
|
16846
|
+
const db = getDatabase();
|
|
16847
|
+
const result = db.prepare("DELETE FROM auth_flows WHERE name = ?").run(name);
|
|
16848
|
+
return result.changes > 0;
|
|
16849
|
+
}
|
|
16850
|
+
function touchAuthFlow(id) {
|
|
16851
|
+
const db = getDatabase();
|
|
16852
|
+
db.prepare("UPDATE auth_flows SET last_used = datetime('now') WHERE id = ?").run(id);
|
|
16853
|
+
}
|
|
16854
|
+
function isAuthPage(url) {
|
|
16855
|
+
return AUTH_URL_PATTERNS.some((pattern) => pattern.test(url));
|
|
16856
|
+
}
|
|
16857
|
+
function isAuthRedirect(fromUrl, toUrl) {
|
|
16858
|
+
if (fromUrl === toUrl)
|
|
16859
|
+
return false;
|
|
16860
|
+
return isAuthPage(toUrl) && !isAuthPage(fromUrl);
|
|
16861
|
+
}
|
|
16862
|
+
async function tryReplayAuth(page, domain) {
|
|
16863
|
+
const flow = getAuthFlowByDomain(domain);
|
|
16864
|
+
if (!flow)
|
|
16865
|
+
return { replayed: false };
|
|
16866
|
+
if (flow.storage_state_path) {
|
|
16867
|
+
try {
|
|
16868
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
|
|
16869
|
+
if (existsSync5(flow.storage_state_path)) {
|
|
16870
|
+
const state = JSON.parse(readFileSync3(flow.storage_state_path, "utf8"));
|
|
16871
|
+
if (state.cookies?.length) {
|
|
16872
|
+
await page.context().addCookies(state.cookies);
|
|
16873
|
+
await page.reload();
|
|
16874
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
16875
|
+
if (!isAuthPage(page.url())) {
|
|
16876
|
+
touchAuthFlow(flow.id);
|
|
16877
|
+
return { replayed: true, flow, method: "storage_state" };
|
|
16878
|
+
}
|
|
16879
|
+
}
|
|
16880
|
+
}
|
|
16881
|
+
} catch {}
|
|
16882
|
+
}
|
|
16883
|
+
if (flow.recording_id) {
|
|
16884
|
+
try {
|
|
16885
|
+
const { replayRecording: replayRecording2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
|
|
16886
|
+
const result = await replayRecording2(flow.recording_id, page);
|
|
16887
|
+
if (result.success) {
|
|
16888
|
+
try {
|
|
16889
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
16890
|
+
const path = await saveStateFromPage2(page, flow.name);
|
|
16891
|
+
const db = getDatabase();
|
|
16892
|
+
db.prepare("UPDATE auth_flows SET storage_state_path = ?, last_used = datetime('now') WHERE id = ?").run(path, flow.id);
|
|
16893
|
+
} catch {}
|
|
16894
|
+
touchAuthFlow(flow.id);
|
|
16895
|
+
return { replayed: true, flow, method: "recording_replay" };
|
|
16896
|
+
}
|
|
16897
|
+
} catch {}
|
|
16898
|
+
}
|
|
16899
|
+
return { replayed: false, flow };
|
|
16900
|
+
}
|
|
16901
|
+
var AUTH_URL_PATTERNS;
|
|
16902
|
+
var init_auth_flow = __esm(() => {
|
|
16903
|
+
init_schema();
|
|
16904
|
+
AUTH_URL_PATTERNS = [
|
|
16905
|
+
/\/login/i,
|
|
16906
|
+
/\/signin/i,
|
|
16907
|
+
/\/sign-in/i,
|
|
16908
|
+
/\/auth/i,
|
|
16909
|
+
/\/sso/i,
|
|
16910
|
+
/\/oauth/i,
|
|
16911
|
+
/\/cas\/login/i,
|
|
16912
|
+
/accounts\.google\.com/i,
|
|
16913
|
+
/login\.microsoftonline\.com/i,
|
|
16914
|
+
/github\.com\/login/i,
|
|
16915
|
+
/auth0\.com/i
|
|
16916
|
+
];
|
|
16917
|
+
});
|
|
16918
|
+
|
|
16919
|
+
// src/lib/vision-fallback.ts
|
|
16920
|
+
var exports_vision_fallback = {};
|
|
16921
|
+
__export(exports_vision_fallback, {
|
|
16922
|
+
findElementByVision: () => findElementByVision,
|
|
16923
|
+
clickByVision: () => clickByVision
|
|
16924
|
+
});
|
|
16925
|
+
async function findElementByVision(page, description, opts) {
|
|
16926
|
+
const model = opts?.model ?? process.env["BROWSER_VISION_MODEL"] ?? DEFAULT_MODEL;
|
|
16927
|
+
const screenshot = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
16928
|
+
const base64 = screenshot.toString("base64");
|
|
16929
|
+
const viewport = page.viewportSize() ?? { width: 1280, height: 720 };
|
|
16930
|
+
const apiKey = process.env["ANTHROPIC_API_KEY"];
|
|
16931
|
+
if (!apiKey) {
|
|
16932
|
+
return { found: false, x: 0, y: 0, confidence: "none", description, model, error: "ANTHROPIC_API_KEY not set" };
|
|
16933
|
+
}
|
|
16934
|
+
try {
|
|
16935
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
16936
|
+
method: "POST",
|
|
16937
|
+
headers: {
|
|
16938
|
+
"content-type": "application/json",
|
|
16939
|
+
"x-api-key": apiKey,
|
|
16940
|
+
"anthropic-version": "2023-06-01"
|
|
16941
|
+
},
|
|
16942
|
+
body: JSON.stringify({
|
|
16943
|
+
model,
|
|
16944
|
+
max_tokens: 256,
|
|
16945
|
+
messages: [{
|
|
16946
|
+
role: "user",
|
|
16947
|
+
content: [
|
|
16948
|
+
{
|
|
16949
|
+
type: "image",
|
|
16950
|
+
source: { type: "base64", media_type: "image/jpeg", data: base64 }
|
|
16951
|
+
},
|
|
16952
|
+
{
|
|
16953
|
+
type: "text",
|
|
16954
|
+
text: `Find the element matching this description: "${description}"
|
|
16955
|
+
|
|
16956
|
+
The screenshot is ${viewport.width}x${viewport.height} pixels.
|
|
16957
|
+
|
|
16958
|
+
Reply with ONLY a JSON object (no markdown, no explanation):
|
|
16959
|
+
{"found": true, "x": <pixel_x>, "y": <pixel_y>, "confidence": "high|medium|low", "description": "<what you found>"}
|
|
16960
|
+
|
|
16961
|
+
If you cannot find the element:
|
|
16962
|
+
{"found": false, "x": 0, "y": 0, "confidence": "none", "description": "not found"}`
|
|
16963
|
+
}
|
|
16964
|
+
]
|
|
16965
|
+
}]
|
|
16966
|
+
})
|
|
16967
|
+
});
|
|
16968
|
+
const data = await response.json();
|
|
16969
|
+
const text = data.content?.[0]?.text ?? "";
|
|
16970
|
+
const jsonStr = text.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
|
|
16971
|
+
const result = JSON.parse(jsonStr);
|
|
16972
|
+
result.model = model;
|
|
16973
|
+
return result;
|
|
16974
|
+
} catch (err) {
|
|
16975
|
+
return {
|
|
16976
|
+
found: false,
|
|
16977
|
+
x: 0,
|
|
16978
|
+
y: 0,
|
|
16979
|
+
confidence: "none",
|
|
16980
|
+
description,
|
|
16981
|
+
model,
|
|
16982
|
+
error: err instanceof Error ? err.message : String(err)
|
|
16983
|
+
};
|
|
16984
|
+
}
|
|
16985
|
+
}
|
|
16986
|
+
async function clickByVision(page, description, opts) {
|
|
16987
|
+
const result = await findElementByVision(page, description, opts);
|
|
16988
|
+
if (result.found && result.x > 0 && result.y > 0) {
|
|
16989
|
+
await page.mouse.click(result.x, result.y);
|
|
16990
|
+
}
|
|
16991
|
+
return result;
|
|
16992
|
+
}
|
|
16993
|
+
var DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
|
|
16994
|
+
|
|
16372
16995
|
// src/lib/auth.ts
|
|
16373
16996
|
var exports_auth = {};
|
|
16374
16997
|
__export(exports_auth, {
|
|
16375
16998
|
loginWithCredentials: () => loginWithCredentials,
|
|
16376
16999
|
getCredentials: () => getCredentials
|
|
16377
17000
|
});
|
|
16378
|
-
import { existsSync as
|
|
16379
|
-
import { join as
|
|
16380
|
-
import { homedir as
|
|
17001
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
17002
|
+
import { join as join9 } from "path";
|
|
17003
|
+
import { homedir as homedir9 } from "os";
|
|
16381
17004
|
async function getCredentials(service) {
|
|
16382
17005
|
try {
|
|
16383
|
-
const { getSecret } = await import(`${
|
|
17006
|
+
const { getSecret } = await import(`${homedir9()}/Workspace/hasna/opensource/opensourcedev/open-secrets/src/store.js`);
|
|
16384
17007
|
const email = getSecret(`${service}_email`) ?? getSecret(`${service}_username`) ?? getSecret(`${service}_login`);
|
|
16385
17008
|
const password = getSecret(`${service}_password`) ?? getSecret(`${service}_pass`);
|
|
16386
17009
|
if (email?.value && password?.value) {
|
|
16387
17010
|
return { email: email.value, password: password.value };
|
|
16388
17011
|
}
|
|
16389
17012
|
} catch {}
|
|
16390
|
-
const secretsPath =
|
|
16391
|
-
if (
|
|
17013
|
+
const secretsPath = join9(homedir9(), ".secrets");
|
|
17014
|
+
if (existsSync5(secretsPath)) {
|
|
16392
17015
|
const content = readFileSync3(secretsPath, "utf8");
|
|
16393
17016
|
const lines = content.split(`
|
|
16394
17017
|
`);
|
|
@@ -16565,14 +17188,14 @@ __export(exports_dist, {
|
|
|
16565
17188
|
InvalidScopeError: () => InvalidScopeError,
|
|
16566
17189
|
EntityNotFoundError: () => EntityNotFoundError,
|
|
16567
17190
|
DuplicateMemoryError: () => DuplicateMemoryError,
|
|
16568
|
-
DEFAULT_MODEL: () =>
|
|
17191
|
+
DEFAULT_MODEL: () => DEFAULT_MODEL2,
|
|
16569
17192
|
DEFAULT_CONFIG: () => DEFAULT_CONFIG
|
|
16570
17193
|
});
|
|
16571
17194
|
import { Database as Database2 } from "bun:sqlite";
|
|
16572
|
-
import { existsSync as
|
|
16573
|
-
import { dirname, join as
|
|
16574
|
-
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as
|
|
16575
|
-
import { homedir as
|
|
17195
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
17196
|
+
import { dirname, join as join10, resolve } from "path";
|
|
17197
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync22, readFileSync as readFileSync4, readdirSync as readdirSync4, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
17198
|
+
import { homedir as homedir10 } from "os";
|
|
16576
17199
|
import { basename as basename2, dirname as dirname2, join as join22, resolve as resolve2 } from "path";
|
|
16577
17200
|
import { existsSync as existsSync32, mkdirSync as mkdirSync32, readFileSync as readFileSync22, writeFileSync as writeFileSync22 } from "fs";
|
|
16578
17201
|
import { homedir as homedir22 } from "os";
|
|
@@ -16586,8 +17209,8 @@ function isInMemoryDb(path) {
|
|
|
16586
17209
|
function findNearestMementosDb(startDir) {
|
|
16587
17210
|
let dir = resolve(startDir);
|
|
16588
17211
|
while (true) {
|
|
16589
|
-
const candidate =
|
|
16590
|
-
if (
|
|
17212
|
+
const candidate = join10(dir, ".mementos", "mementos.db");
|
|
17213
|
+
if (existsSync6(candidate))
|
|
16591
17214
|
return candidate;
|
|
16592
17215
|
const parent = dirname(dir);
|
|
16593
17216
|
if (parent === dir)
|
|
@@ -16599,7 +17222,7 @@ function findNearestMementosDb(startDir) {
|
|
|
16599
17222
|
function findGitRoot(startDir) {
|
|
16600
17223
|
let dir = resolve(startDir);
|
|
16601
17224
|
while (true) {
|
|
16602
|
-
if (
|
|
17225
|
+
if (existsSync6(join10(dir, ".git")))
|
|
16603
17226
|
return dir;
|
|
16604
17227
|
const parent = dirname(dir);
|
|
16605
17228
|
if (parent === dir)
|
|
@@ -16619,25 +17242,25 @@ function getDbPath() {
|
|
|
16619
17242
|
if (process.env["MEMENTOS_DB_SCOPE"] === "project") {
|
|
16620
17243
|
const gitRoot = findGitRoot(cwd);
|
|
16621
17244
|
if (gitRoot) {
|
|
16622
|
-
return
|
|
17245
|
+
return join10(gitRoot, ".mementos", "mementos.db");
|
|
16623
17246
|
}
|
|
16624
17247
|
}
|
|
16625
17248
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
16626
|
-
return
|
|
17249
|
+
return join10(home, ".mementos", "mementos.db");
|
|
16627
17250
|
}
|
|
16628
|
-
function
|
|
17251
|
+
function ensureDir2(filePath) {
|
|
16629
17252
|
if (isInMemoryDb(filePath))
|
|
16630
17253
|
return;
|
|
16631
17254
|
const dir = dirname(resolve(filePath));
|
|
16632
|
-
if (!
|
|
16633
|
-
|
|
17255
|
+
if (!existsSync6(dir)) {
|
|
17256
|
+
mkdirSync9(dir, { recursive: true });
|
|
16634
17257
|
}
|
|
16635
17258
|
}
|
|
16636
17259
|
function getDatabase2(dbPath) {
|
|
16637
17260
|
if (_db2)
|
|
16638
17261
|
return _db2;
|
|
16639
17262
|
const path = dbPath || getDbPath();
|
|
16640
|
-
|
|
17263
|
+
ensureDir2(path);
|
|
16641
17264
|
_db2 = new Database2(path, { create: true });
|
|
16642
17265
|
_db2.run("PRAGMA journal_mode = WAL");
|
|
16643
17266
|
_db2.run("PRAGMA busy_timeout = 5000");
|
|
@@ -18480,7 +19103,7 @@ function isValidCategory(value) {
|
|
|
18480
19103
|
return VALID_CATEGORIES.includes(value);
|
|
18481
19104
|
}
|
|
18482
19105
|
function loadConfig() {
|
|
18483
|
-
const configPath = join22(
|
|
19106
|
+
const configPath = join22(homedir10(), ".mementos", "config.json");
|
|
18484
19107
|
let fileConfig = {};
|
|
18485
19108
|
if (existsSync22(configPath)) {
|
|
18486
19109
|
try {
|
|
@@ -18507,10 +19130,10 @@ function loadConfig() {
|
|
|
18507
19130
|
return merged;
|
|
18508
19131
|
}
|
|
18509
19132
|
function profilesDir() {
|
|
18510
|
-
return join22(
|
|
19133
|
+
return join22(homedir10(), ".mementos", "profiles");
|
|
18511
19134
|
}
|
|
18512
19135
|
function globalConfigPath() {
|
|
18513
|
-
return join22(
|
|
19136
|
+
return join22(homedir10(), ".mementos", "config.json");
|
|
18514
19137
|
}
|
|
18515
19138
|
function readGlobalConfig() {
|
|
18516
19139
|
const p = globalConfigPath();
|
|
@@ -18524,7 +19147,7 @@ function readGlobalConfig() {
|
|
|
18524
19147
|
}
|
|
18525
19148
|
function writeGlobalConfig(data) {
|
|
18526
19149
|
const p = globalConfigPath();
|
|
18527
|
-
|
|
19150
|
+
ensureDir22(dirname2(p));
|
|
18528
19151
|
writeFileSync3(p, JSON.stringify(data, null, 2), "utf-8");
|
|
18529
19152
|
}
|
|
18530
19153
|
function getActiveProfile() {
|
|
@@ -18547,18 +19170,18 @@ function listProfiles2() {
|
|
|
18547
19170
|
const dir = profilesDir();
|
|
18548
19171
|
if (!existsSync22(dir))
|
|
18549
19172
|
return [];
|
|
18550
|
-
return
|
|
19173
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".db")).map((f) => basename2(f, ".db")).sort();
|
|
18551
19174
|
}
|
|
18552
19175
|
function deleteProfile2(name) {
|
|
18553
19176
|
const dbPath = join22(profilesDir(), `${name}.db`);
|
|
18554
19177
|
if (!existsSync22(dbPath))
|
|
18555
19178
|
return false;
|
|
18556
|
-
|
|
19179
|
+
unlinkSync3(dbPath);
|
|
18557
19180
|
if (getActiveProfile() === name)
|
|
18558
19181
|
setActiveProfile(null);
|
|
18559
19182
|
return true;
|
|
18560
19183
|
}
|
|
18561
|
-
function
|
|
19184
|
+
function ensureDir22(dir) {
|
|
18562
19185
|
if (!existsSync22(dir)) {
|
|
18563
19186
|
mkdirSync22(dir, { recursive: true });
|
|
18564
19187
|
}
|
|
@@ -19680,7 +20303,7 @@ function writeConfig(config) {
|
|
|
19680
20303
|
}
|
|
19681
20304
|
function getActiveModel() {
|
|
19682
20305
|
const config = readConfig();
|
|
19683
|
-
return config.activeModel ??
|
|
20306
|
+
return config.activeModel ?? DEFAULT_MODEL2;
|
|
19684
20307
|
}
|
|
19685
20308
|
function setActiveModel(modelId) {
|
|
19686
20309
|
const config = readConfig();
|
|
@@ -19754,7 +20377,7 @@ Return JSON with this exact shape:
|
|
|
19754
20377
|
examples: finalExamples,
|
|
19755
20378
|
count: finalExamples.length
|
|
19756
20379
|
};
|
|
19757
|
-
},
|
|
20380
|
+
}, DEFAULT_MODEL2 = "gpt-4o-mini", CONFIG_DIR, CONFIG_PATH;
|
|
19758
20381
|
var init_dist = __esm(() => {
|
|
19759
20382
|
__defProp2 = Object.defineProperty;
|
|
19760
20383
|
exports_database = {};
|
|
@@ -21190,10 +21813,10 @@ __export(exports_dist2, {
|
|
|
21190
21813
|
acquireLock: () => acquireLock2
|
|
21191
21814
|
});
|
|
21192
21815
|
import { Database as Database3 } from "bun:sqlite";
|
|
21193
|
-
import { mkdirSync as
|
|
21194
|
-
import { join as
|
|
21195
|
-
import { homedir as
|
|
21196
|
-
import { randomUUID as
|
|
21816
|
+
import { mkdirSync as mkdirSync10 } from "fs";
|
|
21817
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
21818
|
+
import { homedir as homedir11 } from "os";
|
|
21819
|
+
import { randomUUID as randomUUID11 } from "crypto";
|
|
21197
21820
|
import { mkdirSync as mkdirSync23, copyFileSync as copyFileSync3, statSync as statSync2 } from "fs";
|
|
21198
21821
|
import { join as join33 } from "path";
|
|
21199
21822
|
import { homedir as homedir33 } from "os";
|
|
@@ -21207,13 +21830,13 @@ import { homedir as homedir42 } from "os";
|
|
|
21207
21830
|
function getDbPath2() {
|
|
21208
21831
|
if (process.env.CONVERSATIONS_DB_PATH)
|
|
21209
21832
|
return process.env.CONVERSATIONS_DB_PATH;
|
|
21210
|
-
return
|
|
21833
|
+
return join11(homedir11(), ".conversations", "messages.db");
|
|
21211
21834
|
}
|
|
21212
21835
|
function getDb() {
|
|
21213
21836
|
if (db)
|
|
21214
21837
|
return db;
|
|
21215
21838
|
const dbPath = getDbPath2();
|
|
21216
|
-
|
|
21839
|
+
mkdirSync10(dirname3(dbPath), { recursive: true });
|
|
21217
21840
|
db = new Database3(dbPath, { create: true });
|
|
21218
21841
|
db.exec("PRAGMA journal_mode = WAL");
|
|
21219
21842
|
db.exec("PRAGMA busy_timeout = 5000");
|
|
@@ -21573,7 +22196,7 @@ function guessMimeType(name) {
|
|
|
21573
22196
|
function sendMessage(opts) {
|
|
21574
22197
|
const db2 = getDb();
|
|
21575
22198
|
const explicitSession = opts.session_id && opts.session_id.trim().length > 0 ? opts.session_id : undefined;
|
|
21576
|
-
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${
|
|
22199
|
+
const sessionId = explicitSession ?? (opts.space ? `space:${opts.space}` : `${[opts.from, opts.to].sort().join("-")}-${randomUUID11().slice(0, 8)}`);
|
|
21577
22200
|
const metadata = opts.metadata ? JSON.stringify(opts.metadata) : null;
|
|
21578
22201
|
const normalizedPriority = opts.priority === "low" || opts.priority === "normal" || opts.priority === "high" || opts.priority === "urgent" ? opts.priority : "normal";
|
|
21579
22202
|
const blocking = opts.blocking ? 1 : 0;
|
|
@@ -25615,11 +26238,11 @@ __export(exports_dist3, {
|
|
|
25615
26238
|
AgentNotFoundError: () => AgentNotFoundError2
|
|
25616
26239
|
});
|
|
25617
26240
|
import { Database as Database4 } from "bun:sqlite";
|
|
25618
|
-
import { existsSync as
|
|
25619
|
-
import { dirname as dirname5, join as
|
|
26241
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync11 } from "fs";
|
|
26242
|
+
import { dirname as dirname5, join as join12, resolve as resolve3 } from "path";
|
|
25620
26243
|
import { existsSync as existsSync33 } from "fs";
|
|
25621
26244
|
import { join as join34 } from "path";
|
|
25622
|
-
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as
|
|
26245
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync24, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync3, writeFileSync as writeFileSync5 } from "fs";
|
|
25623
26246
|
import { join as join24 } from "path";
|
|
25624
26247
|
import { existsSync as existsSync43, readFileSync as readFileSync24, readdirSync as readdirSync22, writeFileSync as writeFileSync23 } from "fs";
|
|
25625
26248
|
import { join as join44 } from "path";
|
|
@@ -25849,8 +26472,8 @@ function isInMemoryDb2(path) {
|
|
|
25849
26472
|
function findNearestTodosDb(startDir) {
|
|
25850
26473
|
let dir = resolve3(startDir);
|
|
25851
26474
|
while (true) {
|
|
25852
|
-
const candidate =
|
|
25853
|
-
if (
|
|
26475
|
+
const candidate = join12(dir, ".todos", "todos.db");
|
|
26476
|
+
if (existsSync7(candidate))
|
|
25854
26477
|
return candidate;
|
|
25855
26478
|
const parent = dirname5(dir);
|
|
25856
26479
|
if (parent === dir)
|
|
@@ -25862,7 +26485,7 @@ function findNearestTodosDb(startDir) {
|
|
|
25862
26485
|
function findGitRoot2(startDir) {
|
|
25863
26486
|
let dir = resolve3(startDir);
|
|
25864
26487
|
while (true) {
|
|
25865
|
-
if (
|
|
26488
|
+
if (existsSync7(join12(dir, ".git")))
|
|
25866
26489
|
return dir;
|
|
25867
26490
|
const parent = dirname5(dir);
|
|
25868
26491
|
if (parent === dir)
|
|
@@ -25882,18 +26505,18 @@ function getDbPath3() {
|
|
|
25882
26505
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
25883
26506
|
const gitRoot = findGitRoot2(cwd);
|
|
25884
26507
|
if (gitRoot) {
|
|
25885
|
-
return
|
|
26508
|
+
return join12(gitRoot, ".todos", "todos.db");
|
|
25886
26509
|
}
|
|
25887
26510
|
}
|
|
25888
26511
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
25889
|
-
return
|
|
26512
|
+
return join12(home, ".todos", "todos.db");
|
|
25890
26513
|
}
|
|
25891
26514
|
function ensureDir3(filePath) {
|
|
25892
26515
|
if (isInMemoryDb2(filePath))
|
|
25893
26516
|
return;
|
|
25894
26517
|
const dir = dirname5(resolve3(filePath));
|
|
25895
|
-
if (!
|
|
25896
|
-
|
|
26518
|
+
if (!existsSync7(dir)) {
|
|
26519
|
+
mkdirSync11(dir, { recursive: true });
|
|
25897
26520
|
}
|
|
25898
26521
|
}
|
|
25899
26522
|
function getDatabase3(dbPath) {
|
|
@@ -26352,14 +26975,14 @@ function ensureProject2(name, path, db2) {
|
|
|
26352
26975
|
}
|
|
26353
26976
|
return createProject3({ name, path }, d);
|
|
26354
26977
|
}
|
|
26355
|
-
function
|
|
26978
|
+
function ensureDir23(dir) {
|
|
26356
26979
|
if (!existsSync23(dir))
|
|
26357
26980
|
mkdirSync24(dir, { recursive: true });
|
|
26358
26981
|
}
|
|
26359
26982
|
function listJsonFiles(dir) {
|
|
26360
26983
|
if (!existsSync23(dir))
|
|
26361
26984
|
return [];
|
|
26362
|
-
return
|
|
26985
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".json"));
|
|
26363
26986
|
}
|
|
26364
26987
|
function readJsonFile(path) {
|
|
26365
26988
|
try {
|
|
@@ -29224,7 +29847,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
|
|
|
29224
29847
|
function pushToClaudeTaskList(taskListId, projectId, options = {}) {
|
|
29225
29848
|
const dir = getTaskListDir(taskListId);
|
|
29226
29849
|
if (!existsSync43(dir))
|
|
29227
|
-
|
|
29850
|
+
ensureDir23(dir);
|
|
29228
29851
|
const filter = {};
|
|
29229
29852
|
if (projectId)
|
|
29230
29853
|
filter["project_id"] = projectId;
|
|
@@ -29442,7 +30065,7 @@ function metadataKey(agent) {
|
|
|
29442
30065
|
function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
|
|
29443
30066
|
const dir = getTaskListDir2(agent, taskListId);
|
|
29444
30067
|
if (!existsSync52(dir))
|
|
29445
|
-
|
|
30068
|
+
ensureDir23(dir);
|
|
29446
30069
|
const filter = {};
|
|
29447
30070
|
if (projectId)
|
|
29448
30071
|
filter["project_id"] = projectId;
|
|
@@ -30970,9 +31593,9 @@ __export(exports_dist4, {
|
|
|
30970
31593
|
CATEGORIES: () => CATEGORIES,
|
|
30971
31594
|
AGENT_TARGETS: () => AGENT_TARGETS
|
|
30972
31595
|
});
|
|
30973
|
-
import { existsSync as
|
|
30974
|
-
import { join as
|
|
30975
|
-
import { homedir as
|
|
31596
|
+
import { existsSync as existsSync8, cpSync, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6, rmSync as rmSync2, readdirSync as readdirSync6, statSync as statSync4, readFileSync as readFileSync7, accessSync, constants } from "fs";
|
|
31597
|
+
import { join as join13, dirname as dirname6 } from "path";
|
|
31598
|
+
import { homedir as homedir12 } from "os";
|
|
30976
31599
|
import { fileURLToPath } from "url";
|
|
30977
31600
|
import { existsSync as existsSync24, readFileSync as readFileSync25, readdirSync as readdirSync23 } from "fs";
|
|
30978
31601
|
import { join as join25 } from "path";
|
|
@@ -31083,35 +31706,35 @@ function normalizeSkillName(name) {
|
|
|
31083
31706
|
function findSkillsDir() {
|
|
31084
31707
|
let dir = __dirname2;
|
|
31085
31708
|
for (let i = 0;i < 5; i++) {
|
|
31086
|
-
const candidate =
|
|
31087
|
-
if (
|
|
31709
|
+
const candidate = join13(dir, "skills");
|
|
31710
|
+
if (existsSync8(candidate)) {
|
|
31088
31711
|
return candidate;
|
|
31089
31712
|
}
|
|
31090
31713
|
dir = dirname6(dir);
|
|
31091
31714
|
}
|
|
31092
|
-
return
|
|
31715
|
+
return join13(__dirname2, "..", "skills");
|
|
31093
31716
|
}
|
|
31094
31717
|
function getSkillPath(name) {
|
|
31095
31718
|
const skillName = normalizeSkillName(name);
|
|
31096
|
-
return
|
|
31719
|
+
return join13(SKILLS_DIR, skillName);
|
|
31097
31720
|
}
|
|
31098
31721
|
function skillExists(name) {
|
|
31099
|
-
return
|
|
31722
|
+
return existsSync8(getSkillPath(name));
|
|
31100
31723
|
}
|
|
31101
31724
|
function installSkill(name, options = {}) {
|
|
31102
31725
|
const { targetDir = process.cwd(), overwrite = false } = options;
|
|
31103
31726
|
const skillName = normalizeSkillName(name);
|
|
31104
31727
|
const sourcePath = getSkillPath(name);
|
|
31105
|
-
const destDir =
|
|
31106
|
-
const destPath =
|
|
31107
|
-
if (!
|
|
31728
|
+
const destDir = join13(targetDir, ".skills");
|
|
31729
|
+
const destPath = join13(destDir, skillName);
|
|
31730
|
+
if (!existsSync8(sourcePath)) {
|
|
31108
31731
|
return {
|
|
31109
31732
|
skill: name,
|
|
31110
31733
|
success: false,
|
|
31111
31734
|
error: `Skill '${name}' not found`
|
|
31112
31735
|
};
|
|
31113
31736
|
}
|
|
31114
|
-
if (
|
|
31737
|
+
if (existsSync8(destPath) && !overwrite) {
|
|
31115
31738
|
return {
|
|
31116
31739
|
skill: name,
|
|
31117
31740
|
success: false,
|
|
@@ -31120,10 +31743,10 @@ function installSkill(name, options = {}) {
|
|
|
31120
31743
|
};
|
|
31121
31744
|
}
|
|
31122
31745
|
try {
|
|
31123
|
-
if (!
|
|
31124
|
-
|
|
31746
|
+
if (!existsSync8(destDir)) {
|
|
31747
|
+
mkdirSync12(destDir, { recursive: true });
|
|
31125
31748
|
}
|
|
31126
|
-
if (
|
|
31749
|
+
if (existsSync8(destPath) && overwrite) {
|
|
31127
31750
|
rmSync2(destPath, { recursive: true, force: true });
|
|
31128
31751
|
}
|
|
31129
31752
|
cpSync(sourcePath, destPath, {
|
|
@@ -31162,10 +31785,10 @@ function installSkills(names, options = {}) {
|
|
|
31162
31785
|
return names.map((name) => installSkill(name, options));
|
|
31163
31786
|
}
|
|
31164
31787
|
function updateSkillsIndex(skillsDir) {
|
|
31165
|
-
const indexPath =
|
|
31788
|
+
const indexPath = join13(skillsDir, "index.ts");
|
|
31166
31789
|
const meta = loadMeta(skillsDir);
|
|
31167
31790
|
const disabledSet = new Set(meta.disabled || []);
|
|
31168
|
-
const skills =
|
|
31791
|
+
const skills = readdirSync6(skillsDir).filter((f) => f.startsWith("skill-") && !f.includes(".") && !disabledSet.has(f.replace("skill-", "")));
|
|
31169
31792
|
const exports = skills.map((s) => {
|
|
31170
31793
|
const name = s.replace("skill-", "").replace(/-/g, "_");
|
|
31171
31794
|
return `export * as ${name} from './${s}/src/index.js';`;
|
|
@@ -31181,11 +31804,11 @@ ${exports}
|
|
|
31181
31804
|
writeFileSync6(indexPath, content);
|
|
31182
31805
|
}
|
|
31183
31806
|
function getMetaPath(skillsDir) {
|
|
31184
|
-
return
|
|
31807
|
+
return join13(skillsDir, ".meta.json");
|
|
31185
31808
|
}
|
|
31186
31809
|
function loadMeta(skillsDir) {
|
|
31187
31810
|
const metaPath2 = getMetaPath(skillsDir);
|
|
31188
|
-
if (
|
|
31811
|
+
if (existsSync8(metaPath2)) {
|
|
31189
31812
|
try {
|
|
31190
31813
|
return JSON.parse(readFileSync7(metaPath2, "utf-8"));
|
|
31191
31814
|
} catch {}
|
|
@@ -31200,8 +31823,8 @@ function recordInstall(skillsDir, name) {
|
|
|
31200
31823
|
const skillName = normalizeSkillName(name);
|
|
31201
31824
|
let version = "unknown";
|
|
31202
31825
|
try {
|
|
31203
|
-
const pkgPath =
|
|
31204
|
-
if (
|
|
31826
|
+
const pkgPath = join13(skillsDir, skillName, "package.json");
|
|
31827
|
+
if (existsSync8(pkgPath)) {
|
|
31205
31828
|
const pkg = JSON.parse(readFileSync7(pkgPath, "utf-8"));
|
|
31206
31829
|
version = pkg.version || "unknown";
|
|
31207
31830
|
}
|
|
@@ -31215,12 +31838,12 @@ function recordRemove(skillsDir, name) {
|
|
|
31215
31838
|
saveMeta(skillsDir, meta);
|
|
31216
31839
|
}
|
|
31217
31840
|
function getInstallMeta(targetDir = process.cwd()) {
|
|
31218
|
-
return loadMeta(
|
|
31841
|
+
return loadMeta(join13(targetDir, ".skills"));
|
|
31219
31842
|
}
|
|
31220
31843
|
function disableSkill(name, targetDir = process.cwd()) {
|
|
31221
|
-
const skillsDir =
|
|
31844
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
31222
31845
|
const skillName = normalizeSkillName(name);
|
|
31223
|
-
if (!
|
|
31846
|
+
if (!existsSync8(join13(skillsDir, skillName)))
|
|
31224
31847
|
return false;
|
|
31225
31848
|
const meta = loadMeta(skillsDir);
|
|
31226
31849
|
const disabled = new Set(meta.disabled || []);
|
|
@@ -31233,7 +31856,7 @@ function disableSkill(name, targetDir = process.cwd()) {
|
|
|
31233
31856
|
return true;
|
|
31234
31857
|
}
|
|
31235
31858
|
function enableSkill(name, targetDir = process.cwd()) {
|
|
31236
|
-
const skillsDir =
|
|
31859
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
31237
31860
|
const meta = loadMeta(skillsDir);
|
|
31238
31861
|
const disabled = new Set(meta.disabled || []);
|
|
31239
31862
|
if (!disabled.has(name))
|
|
@@ -31245,24 +31868,24 @@ function enableSkill(name, targetDir = process.cwd()) {
|
|
|
31245
31868
|
return true;
|
|
31246
31869
|
}
|
|
31247
31870
|
function getDisabledSkills(targetDir = process.cwd()) {
|
|
31248
|
-
const meta = loadMeta(
|
|
31871
|
+
const meta = loadMeta(join13(targetDir, ".skills"));
|
|
31249
31872
|
return meta.disabled || [];
|
|
31250
31873
|
}
|
|
31251
31874
|
function getInstalledSkills(targetDir = process.cwd()) {
|
|
31252
|
-
const skillsDir =
|
|
31253
|
-
if (!
|
|
31875
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
31876
|
+
if (!existsSync8(skillsDir)) {
|
|
31254
31877
|
return [];
|
|
31255
31878
|
}
|
|
31256
|
-
return
|
|
31257
|
-
const fullPath =
|
|
31879
|
+
return readdirSync6(skillsDir).filter((f) => {
|
|
31880
|
+
const fullPath = join13(skillsDir, f);
|
|
31258
31881
|
return f.startsWith("skill-") && statSync4(fullPath).isDirectory();
|
|
31259
31882
|
}).map((f) => f.replace("skill-", ""));
|
|
31260
31883
|
}
|
|
31261
31884
|
function removeSkill(name, targetDir = process.cwd()) {
|
|
31262
31885
|
const skillName = normalizeSkillName(name);
|
|
31263
|
-
const skillsDir =
|
|
31264
|
-
const skillPath =
|
|
31265
|
-
if (!
|
|
31886
|
+
const skillsDir = join13(targetDir, ".skills");
|
|
31887
|
+
const skillPath = join13(skillsDir, skillName);
|
|
31888
|
+
if (!existsSync8(skillPath)) {
|
|
31266
31889
|
return false;
|
|
31267
31890
|
}
|
|
31268
31891
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
@@ -31273,24 +31896,24 @@ function removeSkill(name, targetDir = process.cwd()) {
|
|
|
31273
31896
|
function getAgentSkillsDir(agent, scope = "global", projectDir) {
|
|
31274
31897
|
const agentDir = `.${agent}`;
|
|
31275
31898
|
if (scope === "project") {
|
|
31276
|
-
return
|
|
31899
|
+
return join13(projectDir || process.cwd(), agentDir, "skills");
|
|
31277
31900
|
}
|
|
31278
|
-
return
|
|
31901
|
+
return join13(homedir12(), agentDir, "skills");
|
|
31279
31902
|
}
|
|
31280
31903
|
function getAgentSkillPath(name, agent, scope = "global", projectDir) {
|
|
31281
31904
|
const skillName = normalizeSkillName(name);
|
|
31282
|
-
return
|
|
31905
|
+
return join13(getAgentSkillsDir(agent, scope, projectDir), skillName);
|
|
31283
31906
|
}
|
|
31284
31907
|
function installSkillForAgent(name, options, generateSkillMd) {
|
|
31285
31908
|
const { agent, scope = "global", projectDir } = options;
|
|
31286
31909
|
const skillName = normalizeSkillName(name);
|
|
31287
31910
|
const sourcePath = getSkillPath(name);
|
|
31288
|
-
if (!
|
|
31911
|
+
if (!existsSync8(sourcePath)) {
|
|
31289
31912
|
return { skill: name, success: false, error: `Skill '${name}' not found` };
|
|
31290
31913
|
}
|
|
31291
31914
|
let skillMdContent = null;
|
|
31292
|
-
const skillMdPath =
|
|
31293
|
-
if (
|
|
31915
|
+
const skillMdPath = join13(sourcePath, "SKILL.md");
|
|
31916
|
+
if (existsSync8(skillMdPath)) {
|
|
31294
31917
|
skillMdContent = readFileSync7(skillMdPath, "utf-8");
|
|
31295
31918
|
} else if (generateSkillMd) {
|
|
31296
31919
|
skillMdContent = generateSkillMd(name);
|
|
@@ -31300,8 +31923,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
31300
31923
|
}
|
|
31301
31924
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
31302
31925
|
if (scope === "global") {
|
|
31303
|
-
const agentBaseDir2 =
|
|
31304
|
-
if (!
|
|
31926
|
+
const agentBaseDir2 = join13(homedir12(), `.${agent}`);
|
|
31927
|
+
if (!existsSync8(agentBaseDir2)) {
|
|
31305
31928
|
const agentLabels = {
|
|
31306
31929
|
claude: "Claude Code",
|
|
31307
31930
|
codex: "Codex CLI",
|
|
@@ -31324,8 +31947,8 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
31324
31947
|
}
|
|
31325
31948
|
}
|
|
31326
31949
|
try {
|
|
31327
|
-
|
|
31328
|
-
writeFileSync6(
|
|
31950
|
+
mkdirSync12(destDir, { recursive: true });
|
|
31951
|
+
writeFileSync6(join13(destDir, "SKILL.md"), skillMdContent);
|
|
31329
31952
|
return { skill: name, success: true, path: destDir };
|
|
31330
31953
|
} catch (error) {
|
|
31331
31954
|
return {
|
|
@@ -31338,7 +31961,7 @@ function installSkillForAgent(name, options, generateSkillMd) {
|
|
|
31338
31961
|
function removeSkillForAgent(name, options) {
|
|
31339
31962
|
const { agent, scope = "global", projectDir } = options;
|
|
31340
31963
|
const destDir = getAgentSkillPath(name, agent, scope, projectDir);
|
|
31341
|
-
if (!
|
|
31964
|
+
if (!existsSync8(destDir)) {
|
|
31342
31965
|
return false;
|
|
31343
31966
|
}
|
|
31344
31967
|
rmSync2(destDir, { recursive: true, force: true });
|
|
@@ -33262,7 +33885,7 @@ __export(exports_cron_manager, {
|
|
|
33262
33885
|
deleteCronJob: () => deleteCronJob,
|
|
33263
33886
|
createCronJob: () => createCronJob
|
|
33264
33887
|
});
|
|
33265
|
-
import { randomUUID as
|
|
33888
|
+
import { randomUUID as randomUUID12 } from "crypto";
|
|
33266
33889
|
function ensureCronTable() {
|
|
33267
33890
|
const db2 = getDatabase();
|
|
33268
33891
|
db2.exec(`
|
|
@@ -33291,7 +33914,7 @@ function ensureCronTable() {
|
|
|
33291
33914
|
function createCronJob(schedule, task, name) {
|
|
33292
33915
|
ensureCronTable();
|
|
33293
33916
|
const db2 = getDatabase();
|
|
33294
|
-
const id =
|
|
33917
|
+
const id = randomUUID12();
|
|
33295
33918
|
db2.prepare(`
|
|
33296
33919
|
INSERT INTO cron_jobs (id, name, schedule, task_json, enabled)
|
|
33297
33920
|
VALUES (?, ?, ?, ?, 1)
|
|
@@ -33349,7 +33972,7 @@ function getCronEvents(jobId, limit = 10) {
|
|
|
33349
33972
|
async function executeCronJob(job) {
|
|
33350
33973
|
ensureCronTable();
|
|
33351
33974
|
const db2 = getDatabase();
|
|
33352
|
-
const eventId =
|
|
33975
|
+
const eventId = randomUUID12();
|
|
33353
33976
|
const startedAt = new Date().toISOString();
|
|
33354
33977
|
db2.prepare("INSERT INTO cron_events (id, job_id, started_at) VALUES (?, ?, ?)").run(eventId, job.id, startedAt);
|
|
33355
33978
|
try {
|
|
@@ -33440,7 +34063,7 @@ __export(exports_url_watcher, {
|
|
|
33440
34063
|
deleteWatchJob: () => deleteWatchJob,
|
|
33441
34064
|
createWatchJob: () => createWatchJob
|
|
33442
34065
|
});
|
|
33443
|
-
import { randomUUID as
|
|
34066
|
+
import { randomUUID as randomUUID13 } from "crypto";
|
|
33444
34067
|
import { createHash } from "crypto";
|
|
33445
34068
|
function ensureWatchTables() {
|
|
33446
34069
|
const db2 = getDatabase();
|
|
@@ -33472,7 +34095,7 @@ function ensureWatchTables() {
|
|
|
33472
34095
|
function createWatchJob(url, schedule, opts) {
|
|
33473
34096
|
ensureWatchTables();
|
|
33474
34097
|
const db2 = getDatabase();
|
|
33475
|
-
const id =
|
|
34098
|
+
const id = randomUUID13();
|
|
33476
34099
|
db2.prepare(`
|
|
33477
34100
|
INSERT INTO watch_jobs (id, name, url, schedule, selector, extract_schema, enabled)
|
|
33478
34101
|
VALUES (?, ?, ?, ?, ?, ?, 1)
|
|
@@ -33508,7 +34131,7 @@ function getWatchEvents(watchId, limit = 20) {
|
|
|
33508
34131
|
async function checkWatchJob(job) {
|
|
33509
34132
|
ensureWatchTables();
|
|
33510
34133
|
const db2 = getDatabase();
|
|
33511
|
-
const eventId =
|
|
34134
|
+
const eventId = randomUUID13();
|
|
33512
34135
|
const checkedAt = new Date().toISOString();
|
|
33513
34136
|
let newContent = "";
|
|
33514
34137
|
try {
|
|
@@ -33684,7 +34307,7 @@ var exports_mcp = {};
|
|
|
33684
34307
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
33685
34308
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
33686
34309
|
import { readFileSync as readFileSync8 } from "fs";
|
|
33687
|
-
import { join as
|
|
34310
|
+
import { join as join14 } from "path";
|
|
33688
34311
|
function json(data) {
|
|
33689
34312
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
33690
34313
|
}
|
|
@@ -33749,7 +34372,7 @@ var init_mcp = __esm(async () => {
|
|
|
33749
34372
|
init_dialogs();
|
|
33750
34373
|
init_profiles();
|
|
33751
34374
|
init_types();
|
|
33752
|
-
_pkg = JSON.parse(readFileSync8(
|
|
34375
|
+
_pkg = JSON.parse(readFileSync8(join14(import.meta.dir, "../../package.json"), "utf8"));
|
|
33753
34376
|
networkLogCleanup = new Map;
|
|
33754
34377
|
consoleCaptureCleanup = new Map;
|
|
33755
34378
|
harCaptures = new Map;
|
|
@@ -33757,7 +34380,7 @@ var init_mcp = __esm(async () => {
|
|
|
33757
34380
|
name: "@hasna/browser",
|
|
33758
34381
|
version: "0.0.1"
|
|
33759
34382
|
});
|
|
33760
|
-
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.", {
|
|
34383
|
+
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.", {
|
|
33761
34384
|
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
33762
34385
|
use_case: exports_external.string().optional(),
|
|
33763
34386
|
project_id: exports_external.string().optional(),
|
|
@@ -33768,9 +34391,11 @@ var init_mcp = __esm(async () => {
|
|
|
33768
34391
|
viewport_height: exports_external.number().optional().default(720),
|
|
33769
34392
|
stealth: exports_external.boolean().optional().default(false),
|
|
33770
34393
|
auto_gallery: exports_external.boolean().optional().default(false),
|
|
34394
|
+
storage_state: exports_external.string().optional().describe("Name of saved storage state to load (restores cookies/auth from previous session)"),
|
|
33771
34395
|
force_new: exports_external.boolean().optional().default(false).describe("Force create a new session even if agent already has one"),
|
|
33772
|
-
tags: exports_external.array(exports_external.string()).optional()
|
|
33773
|
-
|
|
34396
|
+
tags: exports_external.array(exports_external.string()).optional(),
|
|
34397
|
+
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")
|
|
34398
|
+
}, 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 }) => {
|
|
33774
34399
|
try {
|
|
33775
34400
|
if (agent_id && !force_new) {
|
|
33776
34401
|
const existing = getActiveSessionForAgent2(agent_id);
|
|
@@ -33786,7 +34411,9 @@ var init_mcp = __esm(async () => {
|
|
|
33786
34411
|
headless,
|
|
33787
34412
|
viewport: { width: viewport_width, height: viewport_height },
|
|
33788
34413
|
stealth,
|
|
33789
|
-
autoGallery: auto_gallery
|
|
34414
|
+
autoGallery: auto_gallery,
|
|
34415
|
+
storageState: storage_state,
|
|
34416
|
+
cdpUrl: cdp_url
|
|
33790
34417
|
});
|
|
33791
34418
|
if (tags?.length) {
|
|
33792
34419
|
const { addSessionTag: addSessionTag2 } = await Promise.resolve().then(() => (init_sessions(), exports_sessions));
|
|
@@ -33946,7 +34573,7 @@ var init_mcp = __esm(async () => {
|
|
|
33946
34573
|
return err(e);
|
|
33947
34574
|
}
|
|
33948
34575
|
});
|
|
33949
|
-
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 }) => {
|
|
34576
|
+
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 }) => {
|
|
33950
34577
|
try {
|
|
33951
34578
|
const sid = resolveSessionId(session_id);
|
|
33952
34579
|
const page = getSessionPage(sid);
|
|
@@ -33957,14 +34584,17 @@ var init_mcp = __esm(async () => {
|
|
|
33957
34584
|
}
|
|
33958
34585
|
if (!selector)
|
|
33959
34586
|
return err(new Error("Either ref or selector is required"));
|
|
33960
|
-
await click(page, selector, { button, timeout });
|
|
33961
|
-
logEvent(sid, "click", { selector, method: "selector" });
|
|
34587
|
+
const healInfo = await click(page, selector, { button, timeout, selfHeal: self_heal });
|
|
34588
|
+
logEvent(sid, "click", { selector, method: healInfo.healed ? "healed" : "selector" });
|
|
34589
|
+
if (healInfo.healed) {
|
|
34590
|
+
return json({ clicked: selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
34591
|
+
}
|
|
33962
34592
|
return json({ clicked: selector, method: "selector" });
|
|
33963
34593
|
} catch (e) {
|
|
33964
34594
|
return errWithScreenshot(e, session_id);
|
|
33965
34595
|
}
|
|
33966
34596
|
});
|
|
33967
|
-
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 }) => {
|
|
34597
|
+
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 }) => {
|
|
33968
34598
|
try {
|
|
33969
34599
|
const sid = resolveSessionId(session_id);
|
|
33970
34600
|
const page = getSessionPage(sid);
|
|
@@ -33975,8 +34605,11 @@ var init_mcp = __esm(async () => {
|
|
|
33975
34605
|
}
|
|
33976
34606
|
if (!selector)
|
|
33977
34607
|
return err(new Error("Either ref or selector is required"));
|
|
33978
|
-
await type(page, selector, text, { clear, delay });
|
|
33979
|
-
logEvent(sid, "type", { selector, text: text.slice(0, 100) });
|
|
34608
|
+
const healInfo = await type(page, selector, text, { clear, delay, selfHeal: self_heal });
|
|
34609
|
+
logEvent(sid, "type", { selector, text: text.slice(0, 100), method: healInfo.healed ? "healed" : "selector" });
|
|
34610
|
+
if (healInfo.healed) {
|
|
34611
|
+
return json({ typed: text, selector, method: "healed", heal_method: healInfo.method, attempts: healInfo.attempts });
|
|
34612
|
+
}
|
|
33980
34613
|
return json({ typed: text, selector, method: "selector" });
|
|
33981
34614
|
} catch (e) {
|
|
33982
34615
|
return errWithScreenshot(e, session_id);
|
|
@@ -34070,20 +34703,32 @@ var init_mcp = __esm(async () => {
|
|
|
34070
34703
|
return err(e);
|
|
34071
34704
|
}
|
|
34072
34705
|
});
|
|
34073
|
-
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 }) => {
|
|
34706
|
+
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 }) => {
|
|
34074
34707
|
try {
|
|
34075
34708
|
const sid = resolveSessionId(session_id);
|
|
34076
34709
|
const page = getSessionPage(sid);
|
|
34077
|
-
|
|
34710
|
+
const text = await getText(page, selector);
|
|
34711
|
+
if (sanitize) {
|
|
34712
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
34713
|
+
const sanitized = sanitizeText2(text);
|
|
34714
|
+
return json({ text: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
34715
|
+
}
|
|
34716
|
+
return json({ text });
|
|
34078
34717
|
} catch (e) {
|
|
34079
34718
|
return err(e);
|
|
34080
34719
|
}
|
|
34081
34720
|
});
|
|
34082
|
-
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 }) => {
|
|
34721
|
+
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 }) => {
|
|
34083
34722
|
try {
|
|
34084
34723
|
const sid = resolveSessionId(session_id);
|
|
34085
34724
|
const page = getSessionPage(sid);
|
|
34086
|
-
|
|
34725
|
+
const html = await getHTML(page, selector);
|
|
34726
|
+
if (sanitize) {
|
|
34727
|
+
const { sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
34728
|
+
const sanitized = sanitizeHTML2(html);
|
|
34729
|
+
return json({ html: sanitized.text, stripped: sanitized.stripped, warnings: sanitized.warnings });
|
|
34730
|
+
}
|
|
34731
|
+
return json({ html });
|
|
34087
34732
|
} catch (e) {
|
|
34088
34733
|
return err(e);
|
|
34089
34734
|
}
|
|
@@ -34098,16 +34743,32 @@ var init_mcp = __esm(async () => {
|
|
|
34098
34743
|
return err(e);
|
|
34099
34744
|
}
|
|
34100
34745
|
});
|
|
34101
|
-
server.tool("browser_extract", "Extract content from the page in a specified format", {
|
|
34746
|
+
server.tool("browser_extract", "Extract content from the page in a specified format. Sanitizes prompt injection by default.", {
|
|
34102
34747
|
session_id: exports_external.string().optional(),
|
|
34103
34748
|
format: exports_external.enum(["text", "html", "links", "table", "structured"]).optional().default("text"),
|
|
34104
34749
|
selector: exports_external.string().optional(),
|
|
34105
|
-
schema: exports_external.record(exports_external.string()).optional()
|
|
34106
|
-
|
|
34750
|
+
schema: exports_external.record(exports_external.string()).optional(),
|
|
34751
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from extracted content (default: true)")
|
|
34752
|
+
}, async ({ session_id, format, selector, schema, sanitize }) => {
|
|
34107
34753
|
try {
|
|
34108
34754
|
const sid = resolveSessionId(session_id);
|
|
34109
34755
|
const page = getSessionPage(sid);
|
|
34110
34756
|
const result = await extract(page, { format, selector, schema });
|
|
34757
|
+
if (sanitize) {
|
|
34758
|
+
const { sanitizeText: sanitizeText2, sanitizeHTML: sanitizeHTML2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
34759
|
+
if (result.text) {
|
|
34760
|
+
const s = sanitizeText2(result.text);
|
|
34761
|
+
result.text = s.text;
|
|
34762
|
+
result.stripped = s.stripped;
|
|
34763
|
+
result.warnings = s.warnings;
|
|
34764
|
+
}
|
|
34765
|
+
if (result.html) {
|
|
34766
|
+
const s = sanitizeHTML2(result.html);
|
|
34767
|
+
result.html = s.text;
|
|
34768
|
+
result.stripped = s.stripped;
|
|
34769
|
+
result.warnings = s.warnings;
|
|
34770
|
+
}
|
|
34771
|
+
}
|
|
34111
34772
|
return json(result);
|
|
34112
34773
|
} catch (e) {
|
|
34113
34774
|
return err(e);
|
|
@@ -34124,17 +34785,27 @@ var init_mcp = __esm(async () => {
|
|
|
34124
34785
|
return err(e);
|
|
34125
34786
|
}
|
|
34126
34787
|
});
|
|
34127
|
-
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.", {
|
|
34788
|
+
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.", {
|
|
34128
34789
|
session_id: exports_external.string().optional(),
|
|
34129
34790
|
compact: exports_external.boolean().optional().default(true),
|
|
34130
34791
|
max_refs: exports_external.number().optional().default(50),
|
|
34131
|
-
full_tree: exports_external.boolean().optional().default(false)
|
|
34132
|
-
|
|
34792
|
+
full_tree: exports_external.boolean().optional().default(false),
|
|
34793
|
+
sanitize: exports_external.boolean().optional().default(true).describe("Strip prompt injection patterns from snapshot text (default: true)")
|
|
34794
|
+
}, async ({ session_id, compact, max_refs, full_tree, sanitize }) => {
|
|
34133
34795
|
try {
|
|
34134
34796
|
const sid = resolveSessionId(session_id);
|
|
34135
34797
|
const page = getSessionPage(sid);
|
|
34136
34798
|
const result = await takeSnapshot(page, sid);
|
|
34137
34799
|
setLastSnapshot(sid, result);
|
|
34800
|
+
let injection_warnings;
|
|
34801
|
+
if (sanitize) {
|
|
34802
|
+
const { sanitizeText: sanitizeText2 } = await Promise.resolve().then(() => (init_sanitize(), exports_sanitize));
|
|
34803
|
+
const sanitized = sanitizeText2(result.tree);
|
|
34804
|
+
if (sanitized.stripped > 0) {
|
|
34805
|
+
injection_warnings = sanitized.warnings;
|
|
34806
|
+
result.tree = sanitized.text;
|
|
34807
|
+
}
|
|
34808
|
+
}
|
|
34138
34809
|
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
34139
34810
|
const limitedRefs = Object.fromEntries(refEntries);
|
|
34140
34811
|
const truncated = Object.keys(result.refs).length > max_refs;
|
|
@@ -34146,27 +34817,29 @@ var init_mcp = __esm(async () => {
|
|
|
34146
34817
|
interactive_count: result.interactive_count,
|
|
34147
34818
|
shown_count: refEntries.length,
|
|
34148
34819
|
truncated,
|
|
34149
|
-
refs: limitedRefs
|
|
34820
|
+
refs: limitedRefs,
|
|
34821
|
+
...injection_warnings ? { injection_warnings } : {}
|
|
34150
34822
|
});
|
|
34151
34823
|
}
|
|
34152
34824
|
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
34153
34825
|
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
34154
|
-
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
34826
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated, ...injection_warnings ? { injection_warnings } : {} });
|
|
34155
34827
|
} catch (e) {
|
|
34156
34828
|
return err(e);
|
|
34157
34829
|
}
|
|
34158
34830
|
});
|
|
34159
|
-
server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements
|
|
34831
|
+
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.", {
|
|
34160
34832
|
session_id: exports_external.string().optional(),
|
|
34161
|
-
selector: exports_external.string().optional(),
|
|
34833
|
+
selector: exports_external.string().optional().describe("CSS selector to screenshot a specific section (e.g. '#main', '.header', 'form')"),
|
|
34162
34834
|
full_page: exports_external.boolean().optional().default(false),
|
|
34163
34835
|
format: exports_external.enum(["png", "jpeg", "webp"]).optional().default("webp"),
|
|
34164
34836
|
quality: exports_external.number().optional().default(60),
|
|
34165
34837
|
max_width: exports_external.number().optional().default(800),
|
|
34166
34838
|
compress: exports_external.boolean().optional().default(true),
|
|
34167
34839
|
thumbnail: exports_external.boolean().optional().default(true),
|
|
34168
|
-
annotate: exports_external.boolean().optional().default(false)
|
|
34169
|
-
|
|
34840
|
+
annotate: exports_external.boolean().optional().default(false),
|
|
34841
|
+
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).")
|
|
34842
|
+
}, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate, detail }) => {
|
|
34170
34843
|
try {
|
|
34171
34844
|
const sid = resolveSessionId(session_id);
|
|
34172
34845
|
const page = getSessionPage(sid);
|
|
@@ -34183,7 +34856,9 @@ var init_mcp = __esm(async () => {
|
|
|
34183
34856
|
annotation_count: annotated.annotations.length
|
|
34184
34857
|
});
|
|
34185
34858
|
}
|
|
34186
|
-
const
|
|
34859
|
+
const effectiveMaxWidth = detail === "high" ? 1280 : max_width;
|
|
34860
|
+
const effectiveQuality = detail === "high" ? 75 : quality;
|
|
34861
|
+
const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality: effectiveQuality, maxWidth: effectiveMaxWidth, compress, thumbnail });
|
|
34187
34862
|
result.url = page.url();
|
|
34188
34863
|
try {
|
|
34189
34864
|
const buf = Buffer.from(result.base64, "base64");
|
|
@@ -34192,12 +34867,12 @@ var init_mcp = __esm(async () => {
|
|
|
34192
34867
|
result.download_id = dl.id;
|
|
34193
34868
|
} catch {}
|
|
34194
34869
|
result.estimated_tokens = Math.ceil(result.base64.length / 4);
|
|
34195
|
-
if (result.base64.length >
|
|
34870
|
+
if (detail !== "high" && result.base64.length > 40000) {
|
|
34196
34871
|
result.base64_truncated = true;
|
|
34197
34872
|
result.full_image_path = result.path;
|
|
34198
34873
|
result.base64 = result.thumbnail_base64 ?? "";
|
|
34199
34874
|
}
|
|
34200
|
-
logEvent(sid, "screenshot", { path: result.path });
|
|
34875
|
+
logEvent(sid, "screenshot", { path: result.path, detail, selector });
|
|
34201
34876
|
return json(result);
|
|
34202
34877
|
} catch (e) {
|
|
34203
34878
|
return err(e);
|
|
@@ -34603,6 +35278,94 @@ var init_mcp = __esm(async () => {
|
|
|
34603
35278
|
return err(e);
|
|
34604
35279
|
}
|
|
34605
35280
|
});
|
|
35281
|
+
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 }) => {
|
|
35282
|
+
try {
|
|
35283
|
+
const sid = resolveSessionId(session_id);
|
|
35284
|
+
const page = getSessionPage(sid);
|
|
35285
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
35286
|
+
const path = await saveStateFromPage2(page, name);
|
|
35287
|
+
return json({ saved: true, name, path });
|
|
35288
|
+
} catch (e) {
|
|
35289
|
+
return err(e);
|
|
35290
|
+
}
|
|
35291
|
+
});
|
|
35292
|
+
server.tool("browser_session_list_states", "List all saved storage states (auth snapshots)", {}, async () => {
|
|
35293
|
+
try {
|
|
35294
|
+
const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
35295
|
+
const states = listStates2();
|
|
35296
|
+
return json({ states, count: states.length });
|
|
35297
|
+
} catch (e) {
|
|
35298
|
+
return err(e);
|
|
35299
|
+
}
|
|
35300
|
+
});
|
|
35301
|
+
server.tool("browser_session_delete_state", "Delete a saved storage state", { name: exports_external.string() }, async ({ name }) => {
|
|
35302
|
+
try {
|
|
35303
|
+
const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
35304
|
+
return json({ deleted: deleteState2(name), name });
|
|
35305
|
+
} catch (e) {
|
|
35306
|
+
return err(e);
|
|
35307
|
+
}
|
|
35308
|
+
});
|
|
35309
|
+
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 }) => {
|
|
35310
|
+
try {
|
|
35311
|
+
const sid = resolveSessionId(session_id);
|
|
35312
|
+
const page = getSessionPage(sid);
|
|
35313
|
+
if (start_url)
|
|
35314
|
+
await navigate(page, start_url);
|
|
35315
|
+
const recording = startRecording(sid, `auth-${name}`, page.url());
|
|
35316
|
+
return json({ recording_id: recording.id, name, message: "Recording started. Perform login, then call browser_auth_stop." });
|
|
35317
|
+
} catch (e) {
|
|
35318
|
+
return err(e);
|
|
35319
|
+
}
|
|
35320
|
+
});
|
|
35321
|
+
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 }) => {
|
|
35322
|
+
try {
|
|
35323
|
+
const sid = resolveSessionId(session_id);
|
|
35324
|
+
const page = getSessionPage(sid);
|
|
35325
|
+
const recording = stopRecording(recording_id);
|
|
35326
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
35327
|
+
const statePath2 = await saveStateFromPage2(page, name);
|
|
35328
|
+
let domain = "";
|
|
35329
|
+
try {
|
|
35330
|
+
domain = new URL(page.url()).hostname;
|
|
35331
|
+
} catch {}
|
|
35332
|
+
const { saveAuthFlow: saveAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
35333
|
+
const flow = saveAuthFlow2({ name, domain, recordingId: recording.id, storageStatePath: statePath2 });
|
|
35334
|
+
return json({ flow, recording_steps: recording.steps.length });
|
|
35335
|
+
} catch (e) {
|
|
35336
|
+
return err(e);
|
|
35337
|
+
}
|
|
35338
|
+
});
|
|
35339
|
+
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 }) => {
|
|
35340
|
+
try {
|
|
35341
|
+
const sid = resolveSessionId(session_id);
|
|
35342
|
+
const page = getSessionPage(sid);
|
|
35343
|
+
const { getAuthFlowByName: getAuthFlowByName2, tryReplayAuth: tryReplayAuth2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
35344
|
+
const flow = getAuthFlowByName2(name);
|
|
35345
|
+
if (!flow)
|
|
35346
|
+
return err(new Error(`Auth flow '${name}' not found`));
|
|
35347
|
+
const result = await tryReplayAuth2(page, flow.domain);
|
|
35348
|
+
return json(result);
|
|
35349
|
+
} catch (e) {
|
|
35350
|
+
return err(e);
|
|
35351
|
+
}
|
|
35352
|
+
});
|
|
35353
|
+
server.tool("browser_auth_list", "List all saved auth flows", {}, async () => {
|
|
35354
|
+
try {
|
|
35355
|
+
const { listAuthFlows: listAuthFlows2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
35356
|
+
return json({ flows: listAuthFlows2() });
|
|
35357
|
+
} catch (e) {
|
|
35358
|
+
return err(e);
|
|
35359
|
+
}
|
|
35360
|
+
});
|
|
35361
|
+
server.tool("browser_auth_delete", "Delete a saved auth flow", { name: exports_external.string() }, async ({ name }) => {
|
|
35362
|
+
try {
|
|
35363
|
+
const { deleteAuthFlow: deleteAuthFlow2 } = await Promise.resolve().then(() => (init_auth_flow(), exports_auth_flow));
|
|
35364
|
+
return json({ deleted: deleteAuthFlow2(name) });
|
|
35365
|
+
} catch (e) {
|
|
35366
|
+
return err(e);
|
|
35367
|
+
}
|
|
35368
|
+
});
|
|
34606
35369
|
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 }) => {
|
|
34607
35370
|
try {
|
|
34608
35371
|
const sid = resolveSessionId(session_id);
|
|
@@ -34613,20 +35376,45 @@ var init_mcp = __esm(async () => {
|
|
|
34613
35376
|
return err(e);
|
|
34614
35377
|
}
|
|
34615
35378
|
});
|
|
34616
|
-
server.tool("browser_fill_form", "Fill multiple form fields in one call. Fields map: { selector: value }. Handles text, checkboxes, selects.", {
|
|
35379
|
+
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.", {
|
|
34617
35380
|
session_id: exports_external.string().optional(),
|
|
34618
35381
|
fields: exports_external.record(exports_external.union([exports_external.string(), exports_external.boolean()])),
|
|
34619
|
-
submit_selector: exports_external.string().optional()
|
|
34620
|
-
|
|
35382
|
+
submit_selector: exports_external.string().optional(),
|
|
35383
|
+
self_heal: exports_external.boolean().optional().default(true).describe("Auto-try fallback selectors if element not found")
|
|
35384
|
+
}, async ({ session_id, fields, submit_selector, self_heal }) => {
|
|
34621
35385
|
try {
|
|
34622
35386
|
const sid = resolveSessionId(session_id);
|
|
34623
35387
|
const page = getSessionPage(sid);
|
|
34624
|
-
const result = await fillForm(page, fields, submit_selector);
|
|
35388
|
+
const result = await fillForm(page, fields, submit_selector, self_heal);
|
|
34625
35389
|
return json(result);
|
|
34626
35390
|
} catch (e) {
|
|
34627
35391
|
return errWithScreenshot(e, session_id);
|
|
34628
35392
|
}
|
|
34629
35393
|
});
|
|
35394
|
+
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.", {
|
|
35395
|
+
session_id: exports_external.string().optional(),
|
|
35396
|
+
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')"),
|
|
35397
|
+
click: exports_external.boolean().optional().default(false).describe("Click the element after finding it"),
|
|
35398
|
+
model: exports_external.string().optional().describe("Vision model to use (default: claude-sonnet-4-5-20250929)")
|
|
35399
|
+
}, async ({ session_id, description, click: doClick, model }) => {
|
|
35400
|
+
try {
|
|
35401
|
+
const sid = resolveSessionId(session_id);
|
|
35402
|
+
const page = getSessionPage(sid);
|
|
35403
|
+
if (doClick) {
|
|
35404
|
+
const { clickByVision: clickByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
35405
|
+
const result = await clickByVision2(page, description, { model });
|
|
35406
|
+
logEvent(sid, "vision_click", { query: description, ...result });
|
|
35407
|
+
return json(result);
|
|
35408
|
+
} else {
|
|
35409
|
+
const { findElementByVision: findElementByVision2 } = await Promise.resolve().then(() => exports_vision_fallback);
|
|
35410
|
+
const result = await findElementByVision2(page, description, { model });
|
|
35411
|
+
logEvent(sid, "vision_find", { query: description, ...result });
|
|
35412
|
+
return json(result);
|
|
35413
|
+
}
|
|
35414
|
+
} catch (e) {
|
|
35415
|
+
return err(e);
|
|
35416
|
+
}
|
|
35417
|
+
});
|
|
34630
35418
|
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 }) => {
|
|
34631
35419
|
try {
|
|
34632
35420
|
const sid = resolveSessionId(session_id);
|
|
@@ -35047,6 +35835,7 @@ var init_mcp = __esm(async () => {
|
|
|
35047
35835
|
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
35048
35836
|
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
35049
35837
|
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
35838
|
+
{ tool: "browser_find_visual", description: "Find element using AI vision (for canvas, images, custom widgets)" },
|
|
35050
35839
|
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
35051
35840
|
],
|
|
35052
35841
|
Extraction: [
|
|
@@ -35075,7 +35864,10 @@ var init_mcp = __esm(async () => {
|
|
|
35075
35864
|
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
35076
35865
|
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
35077
35866
|
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
35078
|
-
{ tool: "browser_profile_delete", description: "Delete a saved profile" }
|
|
35867
|
+
{ tool: "browser_profile_delete", description: "Delete a saved profile" },
|
|
35868
|
+
{ tool: "browser_session_save_state", description: "Save auth state (Playwright storageState) for reuse" },
|
|
35869
|
+
{ tool: "browser_session_list_states", description: "List saved storage states" },
|
|
35870
|
+
{ tool: "browser_session_delete_state", description: "Delete a saved storage state" }
|
|
35079
35871
|
],
|
|
35080
35872
|
Network: [
|
|
35081
35873
|
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
@@ -35099,6 +35891,13 @@ var init_mcp = __esm(async () => {
|
|
|
35099
35891
|
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
35100
35892
|
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
35101
35893
|
],
|
|
35894
|
+
Auth: [
|
|
35895
|
+
{ tool: "browser_auth_record", description: "Start recording a login flow" },
|
|
35896
|
+
{ tool: "browser_auth_stop", description: "Stop recording and save auth flow" },
|
|
35897
|
+
{ tool: "browser_auth_replay", description: "Replay a saved auth flow" },
|
|
35898
|
+
{ tool: "browser_auth_list", description: "List all saved auth flows" },
|
|
35899
|
+
{ tool: "browser_auth_delete", description: "Delete a saved auth flow" }
|
|
35900
|
+
],
|
|
35102
35901
|
Crawl: [
|
|
35103
35902
|
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
35104
35903
|
],
|
|
@@ -35155,7 +35954,8 @@ var init_mcp = __esm(async () => {
|
|
|
35155
35954
|
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
35156
35955
|
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
35157
35956
|
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
35158
|
-
{ tool: "browser_watch_stop", description: "Stop DOM watcher" }
|
|
35957
|
+
{ tool: "browser_watch_stop", description: "Stop DOM watcher" },
|
|
35958
|
+
{ tool: "browser_parallel", description: "Execute actions across multiple sessions in parallel" }
|
|
35159
35959
|
]
|
|
35160
35960
|
};
|
|
35161
35961
|
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
@@ -35438,6 +36238,82 @@ var init_mcp = __esm(async () => {
|
|
|
35438
36238
|
return err(e);
|
|
35439
36239
|
}
|
|
35440
36240
|
});
|
|
36241
|
+
server.tool("browser_parallel", "Execute actions across multiple sessions in parallel. Each action targets a different session. Returns results array.", {
|
|
36242
|
+
actions: exports_external.array(exports_external.object({
|
|
36243
|
+
session_id: exports_external.string().describe("Target session ID"),
|
|
36244
|
+
tool: exports_external.string().describe("Tool name (e.g. browser_navigate, browser_screenshot, browser_click)"),
|
|
36245
|
+
args: exports_external.record(exports_external.unknown()).optional().default({})
|
|
36246
|
+
})),
|
|
36247
|
+
timeout: exports_external.number().optional().default(30000).describe("Timeout per action in ms")
|
|
36248
|
+
}, async ({ actions, timeout }) => {
|
|
36249
|
+
try {
|
|
36250
|
+
const t0 = Date.now();
|
|
36251
|
+
const promises = actions.map(async (action, index) => {
|
|
36252
|
+
try {
|
|
36253
|
+
const sid = action.session_id;
|
|
36254
|
+
const page = getSessionPage(sid);
|
|
36255
|
+
const args = action.args;
|
|
36256
|
+
const toolName = action.tool.replace(/^browser_/, "");
|
|
36257
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeout}ms`)), timeout));
|
|
36258
|
+
const actionPromise = (async () => {
|
|
36259
|
+
switch (toolName) {
|
|
36260
|
+
case "navigate": {
|
|
36261
|
+
await navigate(page, args.url);
|
|
36262
|
+
const title = await page.title();
|
|
36263
|
+
return { url: page.url(), title };
|
|
36264
|
+
}
|
|
36265
|
+
case "screenshot": {
|
|
36266
|
+
const result2 = await takeScreenshot(page, {
|
|
36267
|
+
maxWidth: args.max_width ?? 800,
|
|
36268
|
+
quality: args.quality ?? 60
|
|
36269
|
+
});
|
|
36270
|
+
return { path: result2.path, size_bytes: result2.size_bytes };
|
|
36271
|
+
}
|
|
36272
|
+
case "click": {
|
|
36273
|
+
if (args.selector)
|
|
36274
|
+
await click(page, args.selector);
|
|
36275
|
+
return { clicked: args.selector };
|
|
36276
|
+
}
|
|
36277
|
+
case "type": {
|
|
36278
|
+
if (args.selector && args.text)
|
|
36279
|
+
await type(page, args.selector, args.text);
|
|
36280
|
+
return { typed: args.text };
|
|
36281
|
+
}
|
|
36282
|
+
case "get_text": {
|
|
36283
|
+
const text = await getText(page);
|
|
36284
|
+
return { text: text.slice(0, 1000), length: text.length };
|
|
36285
|
+
}
|
|
36286
|
+
case "get_links": {
|
|
36287
|
+
const links = await getLinks(page);
|
|
36288
|
+
return { links, count: links.length };
|
|
36289
|
+
}
|
|
36290
|
+
case "snapshot": {
|
|
36291
|
+
const snap = await takeSnapshot(page, sid);
|
|
36292
|
+
return { interactive_count: snap.interactive_count, refs_count: Object.keys(snap.refs).length };
|
|
36293
|
+
}
|
|
36294
|
+
case "evaluate": {
|
|
36295
|
+
const result2 = await page.evaluate(args.expression);
|
|
36296
|
+
return { result: result2 };
|
|
36297
|
+
}
|
|
36298
|
+
default:
|
|
36299
|
+
return { error: `Unknown tool: ${action.tool}` };
|
|
36300
|
+
}
|
|
36301
|
+
})();
|
|
36302
|
+
const result = await Promise.race([actionPromise, timeoutPromise]);
|
|
36303
|
+
return { index, session_id: sid, tool: action.tool, success: true, result };
|
|
36304
|
+
} catch (e) {
|
|
36305
|
+
return { index, session_id: action.session_id, tool: action.tool, success: false, error: e instanceof Error ? e.message : String(e) };
|
|
36306
|
+
}
|
|
36307
|
+
});
|
|
36308
|
+
const results = await Promise.all(promises);
|
|
36309
|
+
const duration_ms = Date.now() - t0;
|
|
36310
|
+
const succeeded = results.filter((r) => r.success).length;
|
|
36311
|
+
const failed = results.filter((r) => !r.success).length;
|
|
36312
|
+
return json({ results, duration_ms, succeeded, failed, total: actions.length });
|
|
36313
|
+
} catch (e) {
|
|
36314
|
+
return err(e);
|
|
36315
|
+
}
|
|
36316
|
+
});
|
|
35441
36317
|
server.tool("browser_pool_status", "Get status of the pre-warmed browser session pool.", {}, async () => {
|
|
35442
36318
|
try {
|
|
35443
36319
|
return json({ message: "Session pool not yet implemented in this version. Coming in v0.0.6+", ready: 0, total: 0 });
|
|
@@ -35592,10 +36468,10 @@ __export(exports_snapshots, {
|
|
|
35592
36468
|
deleteSnapshot: () => deleteSnapshot,
|
|
35593
36469
|
createSnapshot: () => createSnapshot
|
|
35594
36470
|
});
|
|
35595
|
-
import { randomUUID as
|
|
36471
|
+
import { randomUUID as randomUUID14 } from "crypto";
|
|
35596
36472
|
function createSnapshot(data) {
|
|
35597
36473
|
const db2 = getDatabase();
|
|
35598
|
-
const id =
|
|
36474
|
+
const id = randomUUID14();
|
|
35599
36475
|
db2.prepare("INSERT INTO snapshots (id, session_id, url, title, html, screenshot_path) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.url, data.title ?? null, data.html ?? null, data.screenshot_path ?? null);
|
|
35600
36476
|
return getSnapshot(id);
|
|
35601
36477
|
}
|
|
@@ -35621,8 +36497,8 @@ var init_snapshots = __esm(() => {
|
|
|
35621
36497
|
|
|
35622
36498
|
// src/server/index.ts
|
|
35623
36499
|
var exports_server = {};
|
|
35624
|
-
import { join as
|
|
35625
|
-
import { existsSync as
|
|
36500
|
+
import { join as join15 } from "path";
|
|
36501
|
+
import { existsSync as existsSync9 } from "fs";
|
|
35626
36502
|
function ok(data, status = 200) {
|
|
35627
36503
|
return new Response(JSON.stringify(data), {
|
|
35628
36504
|
status,
|
|
@@ -35872,14 +36748,14 @@ var init_server = __esm(() => {
|
|
|
35872
36748
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
35873
36749
|
const id = path.split("/")[3];
|
|
35874
36750
|
const entry = getEntry(id);
|
|
35875
|
-
if (!entry?.thumbnail_path || !
|
|
36751
|
+
if (!entry?.thumbnail_path || !existsSync9(entry.thumbnail_path))
|
|
35876
36752
|
return notFound("Thumbnail not found");
|
|
35877
36753
|
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
|
|
35878
36754
|
}
|
|
35879
36755
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
35880
36756
|
const id = path.split("/")[3];
|
|
35881
36757
|
const entry = getEntry(id);
|
|
35882
|
-
if (!entry?.path || !
|
|
36758
|
+
if (!entry?.path || !existsSync9(entry.path))
|
|
35883
36759
|
return notFound("Image not found");
|
|
35884
36760
|
return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
|
|
35885
36761
|
}
|
|
@@ -35907,7 +36783,7 @@ var init_server = __esm(() => {
|
|
|
35907
36783
|
if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
|
|
35908
36784
|
const id = path.split("/")[3];
|
|
35909
36785
|
const file = getDownload(id);
|
|
35910
|
-
if (!file || !
|
|
36786
|
+
if (!file || !existsSync9(file.path))
|
|
35911
36787
|
return notFound("Download not found");
|
|
35912
36788
|
return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
|
|
35913
36789
|
}
|
|
@@ -35915,13 +36791,13 @@ var init_server = __esm(() => {
|
|
|
35915
36791
|
const id = path.split("/")[3];
|
|
35916
36792
|
return ok({ deleted: deleteDownload(id) });
|
|
35917
36793
|
}
|
|
35918
|
-
const dashboardDist =
|
|
35919
|
-
if (
|
|
35920
|
-
const filePath = path === "/" ?
|
|
35921
|
-
if (
|
|
36794
|
+
const dashboardDist = join15(import.meta.dir, "../../dashboard/dist");
|
|
36795
|
+
if (existsSync9(dashboardDist)) {
|
|
36796
|
+
const filePath = path === "/" ? join15(dashboardDist, "index.html") : join15(dashboardDist, path);
|
|
36797
|
+
if (existsSync9(filePath)) {
|
|
35922
36798
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
35923
36799
|
}
|
|
35924
|
-
return new Response(Bun.file(
|
|
36800
|
+
return new Response(Bun.file(join15(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
35925
36801
|
}
|
|
35926
36802
|
if (path === "/" || path === "") {
|
|
35927
36803
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|
|
@@ -35964,9 +36840,9 @@ init_recorder();
|
|
|
35964
36840
|
init_recordings();
|
|
35965
36841
|
init_lightpanda();
|
|
35966
36842
|
import { readFileSync as readFileSync9 } from "fs";
|
|
35967
|
-
import { join as
|
|
36843
|
+
import { join as join16 } from "path";
|
|
35968
36844
|
import chalk from "chalk";
|
|
35969
|
-
var pkg = JSON.parse(readFileSync9(
|
|
36845
|
+
var pkg = JSON.parse(readFileSync9(join16(import.meta.dir, "../../package.json"), "utf8"));
|
|
35970
36846
|
var program2 = new Command;
|
|
35971
36847
|
program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
|
|
35972
36848
|
program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headed", "Run in headed (visible) mode").option("--json", "Output as JSON").action(async (url, opts) => {
|
|
@@ -36100,6 +36976,22 @@ sessionCmd.command("close <id>").description("Close a session").action(async (id
|
|
|
36100
36976
|
await closeSession2(id);
|
|
36101
36977
|
console.log(chalk.green(`\u2713 Session closed: ${id}`));
|
|
36102
36978
|
});
|
|
36979
|
+
sessionCmd.command("save-state <name>").description("Save current session auth state for reuse").requiredOption("--session <id>", "Session ID").action(async (name, opts) => {
|
|
36980
|
+
const page = getSessionPage(opts.session);
|
|
36981
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
36982
|
+
const path = await saveStateFromPage2(page, name);
|
|
36983
|
+
console.log(chalk.green(`\u2713 State saved: ${name}`));
|
|
36984
|
+
console.log(chalk.gray(` Path: ${path}`));
|
|
36985
|
+
});
|
|
36986
|
+
sessionCmd.command("list-states").description("List saved auth states").action(async () => {
|
|
36987
|
+
const { listStates: listStates2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
36988
|
+
const states = listStates2();
|
|
36989
|
+
if (states.length === 0) {
|
|
36990
|
+
console.log(chalk.gray("No saved states"));
|
|
36991
|
+
return;
|
|
36992
|
+
}
|
|
36993
|
+
states.forEach((s) => console.log(`${s.name} ${chalk.gray(s.modified)}`));
|
|
36994
|
+
});
|
|
36103
36995
|
var recordCmd = program2.command("record").description("Manage action recordings");
|
|
36104
36996
|
recordCmd.command("start <name>").description("Start recording actions in a new session").option("--url <url>", "Start URL").option("--engine <engine>", "Browser engine", "auto").option("--headed", "Run in headed (visible) mode").action(async (name, opts) => {
|
|
36105
36997
|
const { session } = await createSession2({ engine: opts.engine, startUrl: opts.url, headless: !opts.headed });
|
|
@@ -36162,6 +37054,19 @@ projectCmd.command("list").description("List all projects").action(() => {
|
|
|
36162
37054
|
projects.forEach((p) => console.log(`${p.id} "${p.name}" ${p.path}`));
|
|
36163
37055
|
}
|
|
36164
37056
|
});
|
|
37057
|
+
program2.command("attach").description("Attach to a running Chrome browser via CDP").option("--port <port>", "Chrome debugging port", "9222").option("--host <host>", "Chrome debugging host", "localhost").option("--json", "Output as JSON").action(async (opts) => {
|
|
37058
|
+
const cdpUrl = `http://${opts.host}:${opts.port}`;
|
|
37059
|
+
const { session, page } = await createSession2({ cdpUrl });
|
|
37060
|
+
const title = await page.title();
|
|
37061
|
+
const url = page.url();
|
|
37062
|
+
if (opts.json) {
|
|
37063
|
+
console.log(JSON.stringify({ session_id: session.id, url, title, cdp_url: cdpUrl }));
|
|
37064
|
+
} else {
|
|
37065
|
+
console.log(chalk.green(`\u2713 Attached to Chrome at ${cdpUrl}`));
|
|
37066
|
+
console.log(chalk.blue(` Session: ${session.id}`));
|
|
37067
|
+
console.log(chalk.blue(` Page: ${title} (${url})`));
|
|
37068
|
+
}
|
|
37069
|
+
});
|
|
36165
37070
|
program2.command("install-browser").description("Install a browser engine").option("--engine <engine>", "Engine to install: lightpanda|chromium", "chromium").action(async (opts) => {
|
|
36166
37071
|
if (opts.engine === "chromium") {
|
|
36167
37072
|
const { execSync: execSync3 } = await import("child_process");
|
|
@@ -36253,11 +37158,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
|
|
|
36253
37158
|
});
|
|
36254
37159
|
galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
|
|
36255
37160
|
const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
|
|
36256
|
-
const { existsSync:
|
|
37161
|
+
const { existsSync: existsSync10 } = await import("fs");
|
|
36257
37162
|
const entries = listEntries2({ limit: 9999 });
|
|
36258
37163
|
let removed = 0;
|
|
36259
37164
|
for (const e of entries) {
|
|
36260
|
-
if (!
|
|
37165
|
+
if (!existsSync10(e.path)) {
|
|
36261
37166
|
deleteEntry2(e.id);
|
|
36262
37167
|
removed++;
|
|
36263
37168
|
}
|