@hasna/browser 0.1.1 → 0.2.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 +2851 -2277
- package/dist/lib/daemon-client.d.ts +16 -0
- package/dist/lib/daemon-client.d.ts.map +1 -0
- package/dist/mcp/index.js +171 -2
- package/dist/server/index.js +9 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon client — connects to a running browser daemon via HTTP.
|
|
3
|
+
* Falls back to null if no daemon is running.
|
|
4
|
+
*/
|
|
5
|
+
export declare function getDaemonPidFile(): string;
|
|
6
|
+
export declare function getDaemonPort(): number;
|
|
7
|
+
export declare function isDaemonRunning(): boolean;
|
|
8
|
+
export declare function getDaemonPid(): number | null;
|
|
9
|
+
export declare function getDaemonStatus(): Promise<{
|
|
10
|
+
running: boolean;
|
|
11
|
+
pid: number | null;
|
|
12
|
+
port: number;
|
|
13
|
+
sessions?: number;
|
|
14
|
+
uptime_ms?: number;
|
|
15
|
+
}>;
|
|
16
|
+
//# sourceMappingURL=daemon-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-client.d.ts","sourceRoot":"","sources":["../../src/lib/daemon-client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AASH,wBAAgB,gBAAgB,IAAI,MAAM,CAAqB;AAC/D,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,wBAAgB,eAAe,IAAI,OAAO,CASzC;AAED,wBAAgB,YAAY,IAAI,MAAM,GAAG,IAAI,CAO5C;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAY9I"}
|
package/dist/mcp/index.js
CHANGED
|
@@ -33001,6 +33001,25 @@ server.tool("browser_session_close", "Close a browser session", { session_id: ex
|
|
|
33001
33001
|
return err(e);
|
|
33002
33002
|
}
|
|
33003
33003
|
});
|
|
33004
|
+
server.tool("browser_session_fork", "Fork a session: create a new session with the same auth state (cookies, storage) and URL as an existing one. Like git branch for browser sessions.", { source_session_id: exports_external.string(), name: exports_external.string().optional() }, async ({ source_session_id, name }) => {
|
|
33005
|
+
try {
|
|
33006
|
+
const sourcePage = getSessionPage(source_session_id);
|
|
33007
|
+
const sourceUrl = sourcePage.url();
|
|
33008
|
+
const tempName = `_fork_${Date.now()}`;
|
|
33009
|
+
const { saveStateFromPage: saveStateFromPage2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33010
|
+
await saveStateFromPage2(sourcePage, tempName);
|
|
33011
|
+
const { session, page } = await createSession2({
|
|
33012
|
+
storageState: tempName,
|
|
33013
|
+
startUrl: sourceUrl,
|
|
33014
|
+
name: name ?? `fork-of-${source_session_id.slice(0, 8)}`
|
|
33015
|
+
});
|
|
33016
|
+
const { deleteState: deleteState2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
|
|
33017
|
+
deleteState2(tempName);
|
|
33018
|
+
return json({ forked_session: session, source_url: sourceUrl });
|
|
33019
|
+
} catch (e) {
|
|
33020
|
+
return err(e);
|
|
33021
|
+
}
|
|
33022
|
+
});
|
|
33004
33023
|
server.tool("browser_session_timeline", "Get chronological action log for a session", { session_id: exports_external.string().optional(), limit: exports_external.number().optional().default(50) }, async ({ session_id, limit }) => {
|
|
33005
33024
|
try {
|
|
33006
33025
|
const sid = resolveSessionId(session_id);
|
|
@@ -33594,6 +33613,52 @@ server.tool("browser_har_stop", "Stop HAR capture and return the HAR data", { se
|
|
|
33594
33613
|
return err(e);
|
|
33595
33614
|
}
|
|
33596
33615
|
});
|
|
33616
|
+
server.tool("browser_intercept_response", "Intercept and modify API responses for testing. Mock data, simulate errors, add latency.", {
|
|
33617
|
+
session_id: exports_external.string().optional(),
|
|
33618
|
+
url_pattern: exports_external.string().describe("URL pattern to intercept (e.g. '**/api/users*')"),
|
|
33619
|
+
action: exports_external.enum(["mock", "delay", "error"]).describe("What to do with matched requests"),
|
|
33620
|
+
mock_body: exports_external.string().optional().describe("Response body for mock action"),
|
|
33621
|
+
mock_content_type: exports_external.string().optional().default("application/json"),
|
|
33622
|
+
status_code: exports_external.number().optional().default(200).describe("HTTP status code (for mock/error)"),
|
|
33623
|
+
delay_ms: exports_external.number().optional().default(3000).describe("Delay in ms (for delay action)")
|
|
33624
|
+
}, async ({ session_id, url_pattern, action, mock_body, mock_content_type, status_code, delay_ms }) => {
|
|
33625
|
+
try {
|
|
33626
|
+
const sid = resolveSessionId(session_id);
|
|
33627
|
+
const page = getSessionPage(sid);
|
|
33628
|
+
await page.route(url_pattern, async (route) => {
|
|
33629
|
+
if (action === "mock") {
|
|
33630
|
+
await route.fulfill({
|
|
33631
|
+
status: status_code,
|
|
33632
|
+
contentType: mock_content_type,
|
|
33633
|
+
body: mock_body ?? "{}"
|
|
33634
|
+
});
|
|
33635
|
+
} else if (action === "error") {
|
|
33636
|
+
await route.fulfill({
|
|
33637
|
+
status: status_code ?? 500,
|
|
33638
|
+
contentType: "application/json",
|
|
33639
|
+
body: JSON.stringify({ error: "Intercepted error", status: status_code })
|
|
33640
|
+
});
|
|
33641
|
+
} else if (action === "delay") {
|
|
33642
|
+
await new Promise((r) => setTimeout(r, delay_ms));
|
|
33643
|
+
await route.continue();
|
|
33644
|
+
}
|
|
33645
|
+
});
|
|
33646
|
+
logEvent(sid, "intercept_set", { url_pattern, action, status_code });
|
|
33647
|
+
return json({ intercepted: true, url_pattern, action });
|
|
33648
|
+
} catch (e) {
|
|
33649
|
+
return err(e);
|
|
33650
|
+
}
|
|
33651
|
+
});
|
|
33652
|
+
server.tool("browser_intercept_clear", "Remove all response intercepts from a session", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
33653
|
+
try {
|
|
33654
|
+
const sid = resolveSessionId(session_id);
|
|
33655
|
+
const page = getSessionPage(sid);
|
|
33656
|
+
await page.unrouteAll({ behavior: "ignoreErrors" });
|
|
33657
|
+
return json({ cleared: true });
|
|
33658
|
+
} catch (e) {
|
|
33659
|
+
return err(e);
|
|
33660
|
+
}
|
|
33661
|
+
});
|
|
33597
33662
|
server.tool("browser_performance", "Get performance metrics for the current page", { session_id: exports_external.string().optional() }, async ({ session_id }) => {
|
|
33598
33663
|
try {
|
|
33599
33664
|
const sid = resolveSessionId(session_id);
|
|
@@ -33626,6 +33691,50 @@ server.tool("browser_performance_deep", "Deep performance analysis: Web Vitals,
|
|
|
33626
33691
|
return err(e);
|
|
33627
33692
|
}
|
|
33628
33693
|
});
|
|
33694
|
+
server.tool("browser_accessibility_audit", "Run accessibility audit on the page. Injects axe-core and returns violations grouped by severity (critical, serious, moderate, minor).", { session_id: exports_external.string().optional(), selector: exports_external.string().optional().describe("Scope audit to a specific element") }, async ({ session_id, selector }) => {
|
|
33695
|
+
try {
|
|
33696
|
+
const sid = resolveSessionId(session_id);
|
|
33697
|
+
const page = getSessionPage(sid);
|
|
33698
|
+
await page.evaluate(`
|
|
33699
|
+
if (!window.axe) {
|
|
33700
|
+
const script = document.createElement('script');
|
|
33701
|
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.10.2/axe.min.js';
|
|
33702
|
+
document.head.appendChild(script);
|
|
33703
|
+
await new Promise((resolve, reject) => {
|
|
33704
|
+
script.onload = resolve;
|
|
33705
|
+
script.onerror = reject;
|
|
33706
|
+
});
|
|
33707
|
+
}
|
|
33708
|
+
`);
|
|
33709
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
33710
|
+
const results = await page.evaluate((sel) => {
|
|
33711
|
+
const opts = {};
|
|
33712
|
+
if (sel)
|
|
33713
|
+
opts.include = [sel];
|
|
33714
|
+
return window.axe.run(opts.include ? { include: [sel] } : document).then((r) => ({
|
|
33715
|
+
violations: r.violations.map((v) => ({
|
|
33716
|
+
id: v.id,
|
|
33717
|
+
impact: v.impact,
|
|
33718
|
+
description: v.description,
|
|
33719
|
+
help: v.help,
|
|
33720
|
+
helpUrl: v.helpUrl,
|
|
33721
|
+
nodes_count: v.nodes.length,
|
|
33722
|
+
selectors: v.nodes.slice(0, 3).map((n) => n.target?.[0] ?? "")
|
|
33723
|
+
})),
|
|
33724
|
+
passes: r.passes.length,
|
|
33725
|
+
violations_count: r.violations.length,
|
|
33726
|
+
incomplete: r.incomplete.length
|
|
33727
|
+
}));
|
|
33728
|
+
}, selector);
|
|
33729
|
+
const byImpact = { critical: 0, serious: 0, moderate: 0, minor: 0 };
|
|
33730
|
+
for (const v of results.violations) {
|
|
33731
|
+
byImpact[v.impact] = (byImpact[v.impact] || 0) + 1;
|
|
33732
|
+
}
|
|
33733
|
+
return json({ ...results, by_impact: byImpact, score: Math.max(0, 100 - results.violations_count * 5) });
|
|
33734
|
+
} catch (e) {
|
|
33735
|
+
return err(e);
|
|
33736
|
+
}
|
|
33737
|
+
});
|
|
33629
33738
|
server.tool("browser_console_log", "Get captured console messages for a session", { session_id: exports_external.string().optional(), level: exports_external.enum(["log", "warn", "error", "debug", "info"]).optional() }, async ({ session_id, level }) => {
|
|
33630
33739
|
try {
|
|
33631
33740
|
const sid = resolveSessionId(session_id);
|
|
@@ -34027,6 +34136,61 @@ server.tool("browser_find_visual", "Find an element using AI vision when selecto
|
|
|
34027
34136
|
return err(e);
|
|
34028
34137
|
}
|
|
34029
34138
|
});
|
|
34139
|
+
server.tool("browser_wait_for_idle", "Wait until no network requests are in-flight for a specified duration. Essential for SPAs that load data after navigation.", {
|
|
34140
|
+
session_id: exports_external.string().optional(),
|
|
34141
|
+
idle_time: exports_external.number().optional().default(2000).describe("How long (ms) network must be idle to consider page loaded"),
|
|
34142
|
+
timeout: exports_external.number().optional().default(30000).describe("Max wait time (ms) before giving up")
|
|
34143
|
+
}, async ({ session_id, idle_time, timeout }) => {
|
|
34144
|
+
try {
|
|
34145
|
+
const sid = resolveSessionId(session_id);
|
|
34146
|
+
const page = getSessionPage(sid);
|
|
34147
|
+
const t0 = Date.now();
|
|
34148
|
+
let lastActivity = Date.now();
|
|
34149
|
+
let pending = 0;
|
|
34150
|
+
const onRequest = () => {
|
|
34151
|
+
pending++;
|
|
34152
|
+
lastActivity = Date.now();
|
|
34153
|
+
};
|
|
34154
|
+
const onResponse = () => {
|
|
34155
|
+
pending = Math.max(0, pending - 1);
|
|
34156
|
+
if (pending === 0)
|
|
34157
|
+
lastActivity = Date.now();
|
|
34158
|
+
};
|
|
34159
|
+
const onFailed = () => {
|
|
34160
|
+
pending = Math.max(0, pending - 1);
|
|
34161
|
+
if (pending === 0)
|
|
34162
|
+
lastActivity = Date.now();
|
|
34163
|
+
};
|
|
34164
|
+
page.on("request", onRequest);
|
|
34165
|
+
page.on("response", onResponse);
|
|
34166
|
+
page.on("requestfailed", onFailed);
|
|
34167
|
+
try {
|
|
34168
|
+
await new Promise((resolve4, reject) => {
|
|
34169
|
+
const check = () => {
|
|
34170
|
+
const now3 = Date.now();
|
|
34171
|
+
if (now3 - t0 > timeout) {
|
|
34172
|
+
reject(new Error(`Timeout after ${timeout}ms (${pending} requests still pending)`));
|
|
34173
|
+
return;
|
|
34174
|
+
}
|
|
34175
|
+
if (pending === 0 && now3 - lastActivity >= idle_time) {
|
|
34176
|
+
resolve4();
|
|
34177
|
+
return;
|
|
34178
|
+
}
|
|
34179
|
+
setTimeout(check, 100);
|
|
34180
|
+
};
|
|
34181
|
+
check();
|
|
34182
|
+
});
|
|
34183
|
+
} finally {
|
|
34184
|
+
page.removeListener("request", onRequest);
|
|
34185
|
+
page.removeListener("response", onResponse);
|
|
34186
|
+
page.removeListener("requestfailed", onFailed);
|
|
34187
|
+
}
|
|
34188
|
+
const waited_ms = Date.now() - t0;
|
|
34189
|
+
return json({ idle: true, waited_ms, pending_requests: 0 });
|
|
34190
|
+
} catch (e) {
|
|
34191
|
+
return err(e);
|
|
34192
|
+
}
|
|
34193
|
+
});
|
|
34030
34194
|
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 }) => {
|
|
34031
34195
|
try {
|
|
34032
34196
|
const sid = resolveSessionId(session_id);
|
|
@@ -34494,7 +34658,8 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
34494
34658
|
{ tool: "browser_back", description: "Navigate back in history" },
|
|
34495
34659
|
{ tool: "browser_forward", description: "Navigate forward in history" },
|
|
34496
34660
|
{ tool: "browser_reload", description: "Reload the current page" },
|
|
34497
|
-
{ tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
|
|
34661
|
+
{ tool: "browser_wait_for_navigation", description: "Wait for URL change after action" },
|
|
34662
|
+
{ tool: "browser_wait_for_idle", description: "Wait for network idle (no pending requests)" }
|
|
34498
34663
|
],
|
|
34499
34664
|
Interaction: [
|
|
34500
34665
|
{ tool: "browser_click", description: "Click element by ref or selector" },
|
|
@@ -34547,7 +34712,9 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
34547
34712
|
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
34548
34713
|
{ tool: "browser_network_intercept", description: "Add a network interception rule" },
|
|
34549
34714
|
{ tool: "browser_har_start", description: "Start HAR capture" },
|
|
34550
|
-
{ tool: "browser_har_stop", description: "Stop HAR capture and get data" }
|
|
34715
|
+
{ tool: "browser_har_stop", description: "Stop HAR capture and get data" },
|
|
34716
|
+
{ tool: "browser_intercept_response", description: "Mock/delay/error API responses for testing" },
|
|
34717
|
+
{ tool: "browser_intercept_clear", description: "Remove all response intercepts" }
|
|
34551
34718
|
],
|
|
34552
34719
|
Performance: [
|
|
34553
34720
|
{ tool: "browser_performance", description: "Get performance metrics" }
|
|
@@ -34630,6 +34797,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
34630
34797
|
{ tool: "browser_session_untag", description: "Remove a tag from a session" },
|
|
34631
34798
|
{ tool: "browser_session_stats", description: "Get session stats and token usage" },
|
|
34632
34799
|
{ tool: "browser_session_timeline", description: "Get chronological action log" },
|
|
34800
|
+
{ tool: "browser_session_fork", description: "Fork a session (same auth state + URL)" },
|
|
34633
34801
|
{ tool: "browser_tab_new", description: "Open a new tab" },
|
|
34634
34802
|
{ tool: "browser_tab_list", description: "List all open tabs" },
|
|
34635
34803
|
{ tool: "browser_tab_switch", description: "Switch to a tab by index" },
|
|
@@ -34641,6 +34809,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
34641
34809
|
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
34642
34810
|
{ tool: "browser_detect_env", description: "Detect environment (prod/dev/staging/local)" },
|
|
34643
34811
|
{ tool: "browser_performance_deep", description: "Deep performance: resources, third-party, DOM, memory" },
|
|
34812
|
+
{ tool: "browser_accessibility_audit", description: "Run axe-core accessibility audit with severity breakdown" },
|
|
34644
34813
|
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
34645
34814
|
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
34646
34815
|
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
package/dist/server/index.js
CHANGED
|
@@ -9142,6 +9142,7 @@ async function diffImages(path1, path2) {
|
|
|
9142
9142
|
|
|
9143
9143
|
// src/server/index.ts
|
|
9144
9144
|
var PORT = parseInt(process.env["BROWSER_SERVER_PORT"] ?? "7030");
|
|
9145
|
+
var startTime = Date.now();
|
|
9145
9146
|
var CORS_HEADERS = {
|
|
9146
9147
|
"Access-Control-Allow-Origin": "*",
|
|
9147
9148
|
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
|
@@ -9185,6 +9186,14 @@ var server = Bun.serve({
|
|
|
9185
9186
|
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
|
9186
9187
|
}
|
|
9187
9188
|
try {
|
|
9189
|
+
if (path === "/health" && method === "GET") {
|
|
9190
|
+
const activeSessions = listSessions2({ status: "active" });
|
|
9191
|
+
return ok({
|
|
9192
|
+
status: "ok",
|
|
9193
|
+
active_sessions: activeSessions.length,
|
|
9194
|
+
uptime_ms: Date.now() - startTime
|
|
9195
|
+
});
|
|
9196
|
+
}
|
|
9188
9197
|
if (path === "/api/sessions" && method === "GET") {
|
|
9189
9198
|
const status = url.searchParams.get("status");
|
|
9190
9199
|
const projectId = url.searchParams.get("project_id") ?? undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "General-purpose browser agent toolkit — Playwright, Chrome DevTools Protocol, Lightpanda with auto engine selection. CLI + MCP + REST + SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|