@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.
@@ -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" },
@@ -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.1.1",
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",