pi-lean-dimension 0.1.0 → 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.
Files changed (130) hide show
  1. package/README.md +96 -36
  2. package/node_modules/pi-lean-portal/AGENTS.md +146 -4
  3. package/node_modules/pi-lean-portal/README.md +112 -50
  4. package/node_modules/pi-lean-portal/__tests__/browser-data.test.ts +124 -0
  5. package/node_modules/pi-lean-portal/__tests__/browser-inspect.test.ts +192 -16
  6. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -3
  7. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +49 -35
  8. package/node_modules/pi-lean-portal/__tests__/chromium-py-persistence.test.ts +21 -195
  9. package/node_modules/pi-lean-portal/__tests__/chromium-py.test.ts +17 -81
  10. package/node_modules/pi-lean-portal/__tests__/chromium.test.ts +25 -0
  11. package/node_modules/pi-lean-portal/__tests__/contributed/invisible-py/invisible-py.test.ts +299 -0
  12. package/node_modules/pi-lean-portal/__tests__/cookie-persistence.test.ts +22 -182
  13. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +1 -1
  14. package/node_modules/pi-lean-portal/__tests__/firefox-py-persistence.test.ts +21 -184
  15. package/node_modules/pi-lean-portal/__tests__/firefox-py.test.ts +17 -101
  16. package/node_modules/pi-lean-portal/__tests__/firefox.test.ts +2 -18
  17. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  18. package/node_modules/pi-lean-portal/__tests__/helpers/create-py-backend-harness.ts +105 -0
  19. package/node_modules/pi-lean-portal/__tests__/helpers/load-plugin-config-from-file.ts +53 -0
  20. package/node_modules/pi-lean-portal/__tests__/helpers/mock-plugin.ts +11 -7
  21. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +4 -0
  22. package/node_modules/pi-lean-portal/__tests__/helpers/persistence-suite.ts +218 -0
  23. package/node_modules/pi-lean-portal/__tests__/helpers/plugin-contract.ts +198 -318
  24. package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +198 -0
  25. package/node_modules/pi-lean-portal/__tests__/helpers/test-server.ts +14 -0
  26. package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +150 -15
  27. package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +120 -18
  28. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +6 -67
  29. package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +236 -0
  30. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +401 -11
  31. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +4 -1
  32. package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +318 -0
  33. package/node_modules/pi-lean-portal/__tests__/session-manager.test.ts +50 -0
  34. package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +2 -2
  35. package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +1 -1
  36. package/node_modules/pi-lean-portal/backends/chromium/index.ts +5 -13
  37. package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
  38. package/node_modules/pi-lean-portal/backends/chromium-py/bridge.py +0 -2
  39. package/node_modules/pi-lean-portal/backends/firefox/index.ts +6 -5
  40. package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
  41. package/node_modules/pi-lean-portal/backends/firefox-py/bridge.py +7 -6
  42. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +241 -398
  43. package/node_modules/pi-lean-portal/backends/python-adapter.ts +182 -83
  44. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +1 -42
  45. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
  46. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
  47. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-312.pyc +0 -0
  48. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/accessibility.cpython-313.pyc +0 -0
  49. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-312.pyc +0 -0
  50. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bot_detection.cpython-313.pyc +0 -0
  51. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
  52. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
  53. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-312.pyc +0 -0
  54. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/browser_data.cpython-313.pyc +0 -0
  55. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-312.pyc +0 -0
  56. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
  57. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  58. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  59. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  60. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  61. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/accessibility.py +12 -147
  62. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bot_detection.py +12 -37
  63. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +249 -322
  64. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/browser_data.py +92 -0
  65. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/patch_playwright.py +321 -0
  66. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +511 -299
  67. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
  68. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_accessibility.cpython-313-pytest-9.1.0.pyc → test_accessibility.cpython-313-pytest-9.1.1.pyc} +0 -0
  69. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
  70. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
  71. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
  72. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313-pytest-9.1.1.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_chromium_py_bridge.cpython-313-pytest-9.1.0.pyc → test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_firefox_py_bridge.cpython-313-pytest-9.1.0.pyc → test_firefox_py_bridge.cpython-313-pytest-9.1.1.pyc} +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  77. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/{test_transport.cpython-313-pytest-9.1.0.pyc → test_transport.cpython-313-pytest-9.1.1.pyc} +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/conftest.py +95 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/test_accessibility.py +8 -132
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_bot_detection.py +8 -147
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_browser_data.py +131 -0
  84. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +768 -0
  85. package/node_modules/pi-lean-portal/backends/python-base/tests/test_py_bridges.py +198 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +33 -69
  87. package/node_modules/pi-lean-portal/contributed/CHOOSING.md +126 -0
  88. package/node_modules/pi-lean-portal/contributed/README.md +304 -0
  89. package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +216 -0
  90. package/node_modules/pi-lean-portal/contributed/invisible-py/__pycache__/bridge.cpython-313.pyc +0 -0
  91. package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +434 -0
  92. package/node_modules/pi-lean-portal/core/fetch-backend.ts +0 -5
  93. package/node_modules/pi-lean-portal/core/plugin-api.ts +6 -33
  94. package/node_modules/pi-lean-portal/core/plugin-config.ts +75 -58
  95. package/node_modules/pi-lean-portal/core/plugin-registry.ts +11 -49
  96. package/node_modules/pi-lean-portal/core/router.ts +59 -98
  97. package/node_modules/pi-lean-portal/core/shared/accessibility-tree.ts +10 -143
  98. package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +31 -77
  99. package/node_modules/pi-lean-portal/core/shared/browser-data.json +183 -0
  100. package/node_modules/pi-lean-portal/core/shared/browser-data.ts +50 -0
  101. package/node_modules/pi-lean-portal/core/shared/browser-events.ts +6 -6
  102. package/node_modules/pi-lean-portal/core/shared/dom-extractor.ts +97 -36
  103. package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +12 -15
  104. package/node_modules/pi-lean-portal/core/shared/paths.ts +3 -0
  105. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +7 -21
  106. package/node_modules/pi-lean-portal/{verify-ship-manifest.ts → core/shared/ship-manifest.ts} +34 -9
  107. package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +7 -6
  108. package/node_modules/pi-lean-portal/core/shared/storage-state.ts +40 -9
  109. package/node_modules/pi-lean-portal/index.ts +42 -7
  110. package/node_modules/pi-lean-portal/package.json +8 -3
  111. package/node_modules/pi-lean-portal/ship-manifest.test.ts +8 -3
  112. package/node_modules/pi-lean-portal/tools/browser-inspect.ts +2 -6
  113. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +7 -5
  114. package/node_modules/pi-lean-portal/tools/browser-snapshot.ts +2 -5
  115. package/node_modules/pi-lean-portal/tools/utils.ts +22 -4
  116. package/node_modules/pi-lean-portal/tools/web-fetch.ts +3 -4
  117. package/node_modules/pi-lean-search/README.md +6 -2
  118. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +11 -0
  119. package/node_modules/pi-lean-search/index.ts +4 -21
  120. package/node_modules/pi-lean-search/package.json +2 -2
  121. package/node_modules/pi-lean-search/verify-ship-manifest.ts +6 -92
  122. package/node_modules/pi-lean-search/web-search-tool.ts +19 -13
  123. package/package.json +4 -4
  124. package/node_modules/pi-lean-portal/__tests__/helpers/reddit-fixture.ts +0 -264
  125. package/node_modules/pi-lean-portal/__tests__/helpers/toggle-test-utils.ts +0 -31
  126. package/node_modules/pi-lean-portal/__tests__/reddit-dialog.test.ts +0 -302
  127. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/occlusion.cpython-313.pyc +0 -0
  128. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313-pytest-9.1.0.pyc +0 -0
  129. package/node_modules/pi-lean-portal/backends/python-base/tests/test_chromium_py_bridge.py +0 -281
  130. package/node_modules/pi-lean-portal/backends/python-base/tests/test_firefox_py_bridge.py +0 -212
@@ -27,9 +27,10 @@ import {
27
27
  import { sessionManager } from "../../core/shared/session-manager.js";
28
28
  import { checkPage } from "../../core/shared/bot-detection.js";
29
29
  import { waitForNavigationSettle } from "../../core/shared/nav-settle.js";
30
+ import { NAV_SETTLE } from "../../core/shared/browser-data.js";
30
31
  import { join } from "node:path";
31
32
  import { mkdirSync } from "node:fs";
32
- import { saveStorageState } from "../../core/shared/storage-state.js";
33
+ import { persistSessionState } from "../../core/shared/storage-state.js";
33
34
  import type {
34
35
  BrowserPlugin,
35
36
  PluginCapabilities,
@@ -64,7 +65,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
64
65
  /** Unique stable identifier (e.g. "chromium", "firefox") */
65
66
  abstract readonly name: string;
66
67
 
67
- /** Advertised capabilities */
68
68
  abstract readonly capabilities: PluginCapabilities;
69
69
 
70
70
  /**
@@ -95,18 +95,38 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
95
95
  */
96
96
  protected readonly captureUserAgent: boolean = false;
97
97
 
98
- // ── Private state ──────────────────────────────────────────
99
-
100
- /** Enable structured debug logging via BROWSER_DEBUG env var */
101
98
  private readonly _debug = process.env.BROWSER_DEBUG === "1";
102
99
 
103
- /** Log a structured debug line to stderr when BROWSER_DEBUG=1 */
104
100
  private _log(event: string, data: Record<string, unknown>): void {
105
101
  if (this._debug) {
106
102
  process.stderr.write(`[browser] ${event}: ${JSON.stringify(data)}\n`);
107
103
  }
108
104
  }
109
105
 
106
+ /**
107
+ * Wrap an async operation with automatic success/failure debug logging.
108
+ * Runs the body, then logs based on the result. Body should catch its own
109
+ * errors and return `{ success: false, error }` — an uncaught throw skips
110
+ * the log (but shouldn't happen in normal operation).
111
+ */
112
+ private async _logOp<T extends { success: boolean; error?: string }>(
113
+ op: string,
114
+ ctx: Record<string, unknown>,
115
+ body: () => Promise<T>,
116
+ getExtra?: (result: T) => Record<string, unknown>,
117
+ ): Promise<T> {
118
+ const result = await body();
119
+ if (this._debug) {
120
+ this._log(op, {
121
+ ...ctx,
122
+ ...(getExtra ? getExtra(result) : {}),
123
+ success: result.success,
124
+ ...(result.error ? { error: result.error } : {}),
125
+ });
126
+ }
127
+ return result;
128
+ }
129
+
110
130
  /** Shared browser instance (lazy-initialised) */
111
131
  private _browser: Browser | null = null;
112
132
 
@@ -273,15 +293,22 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
273
293
  if (!this._browser) {
274
294
  this._browser = await this._launchWithHint();
275
295
 
276
- // Auto-recover from browser crash/disconnect
277
- this._browser.on("disconnected", () => {
278
- this._browser = null;
296
+ // Auto-recover from browser crash/disconnect.
297
+ // Extracted as a named function for readability. On disconnect:
298
+ // mark all sessions crashed, clear caches, and null `_browser`
299
+ // so the next navigate() relaunches via lazy-init (which attaches
300
+ // a fresh handler to the new browser).
301
+ const onDisconnected = () => {
302
+ // All Page/Context objects are dead once the Browser
303
+ // disconnects — mark sessions crashed and clear caches.
279
304
  for (const tid of this._pages.keys()) {
280
305
  sessionManager.updateSession(tid, { crashed: true });
281
306
  this._elementCache.delete(tid);
282
307
  }
283
308
  this._pages.clear();
284
- });
309
+ this._browser = null;
310
+ };
311
+ this._browser.on("disconnected", onDisconnected);
285
312
 
286
313
  // UA capture at first launch (Firefox opt-in)
287
314
  if (this.captureUserAgent) {
@@ -395,25 +422,9 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
395
422
  context: BrowserContext,
396
423
  ): Promise<{ cookies: unknown[]; origins: unknown[] } | undefined> {
397
424
  const session = sessionManager.getSession(taskId);
398
- if (!session?.persistState) return undefined;
399
-
400
- try {
401
- const state = await context.storageState();
402
- const name = session.profileName ?? "default";
403
- saveStorageState(name, state);
404
- return state;
405
- } catch (err) {
406
- console.warn(
407
- `[pi-lean-portal] Failed to auto-save storage state for profile ` +
408
- `'${session.profileName ?? "default"}': ` +
409
- `${err instanceof Error ? err.message : String(err)}. ` +
410
- "Session state may be lost.",
411
- );
412
- return undefined;
413
- }
425
+ return persistSessionState(session, () => context.storageState());
414
426
  }
415
427
 
416
- /** Take an accessibility snapshot and update the element cache. */
417
428
  private async takeSnapshot(
418
429
  taskId: string,
419
430
  page: Page,
@@ -465,12 +476,12 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
465
476
  private async checkBotDetection(page: Page): Promise<boolean> {
466
477
  try {
467
478
  const title = await page.title();
468
- const bodyText = await page.evaluate(
469
- () => document.body?.innerText || "",
479
+ const bodyText: string = await page.evaluate(
480
+ "document.body?.innerText || ''",
470
481
  );
471
482
  // Also grab raw HTML to check for CAPTCHA widget embed codes.
472
- const html = await page.evaluate(
473
- () => document.documentElement?.innerHTML || "",
483
+ const html: string = await page.evaluate(
484
+ "document.documentElement?.innerHTML || ''",
474
485
  );
475
486
  // checkPage handles all three: title (challenge phrases),
476
487
  // body (challenge phrases + CDN patterns), and HTML (CAPTCHA embeds).
@@ -493,7 +504,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
493
504
  profileMode?: "none" | "session" | "named";
494
505
  },
495
506
  ): Promise<NavigateResult> {
496
- const _start = performance.now();
497
507
  try {
498
508
  const ctxOpts: {
499
509
  storageState?: unknown;
@@ -546,19 +556,9 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
546
556
 
547
557
  // Wait for dynamic content to settle
548
558
  try {
549
- await page.waitForFunction(
550
- () =>
551
- new Promise<boolean>((resolve) => {
552
- const count = document.querySelectorAll("*").length;
553
- setTimeout(() => {
554
- resolve(
555
- document.querySelectorAll("*").length === count ||
556
- count > 5000,
557
- );
558
- }, 400);
559
- }),
560
- { timeout: 5000 },
561
- );
559
+ await page.waitForFunction(NAV_SETTLE.domStabilizationJs, {
560
+ timeout: NAV_SETTLE.navTimeoutMs,
561
+ });
562
562
  } catch {
563
563
  // Stabilization timed out — proceed with whatever is rendered
564
564
  }
@@ -589,7 +589,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
589
589
  success: true,
590
590
  botDetected: botDetected ?? false,
591
591
  elementCount,
592
- time: Math.round(performance.now() - _start),
593
592
  });
594
593
 
595
594
  return {
@@ -632,7 +631,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
632
631
  botDetected: botDetected ?? false,
633
632
  elementCount: 0,
634
633
  error: msg,
635
- time: Math.round(performance.now() - _start),
636
634
  });
637
635
 
638
636
  return {
@@ -648,7 +646,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
648
646
  }
649
647
 
650
648
  async snapshot(taskId: string): Promise<SnapshotResult> {
651
- const _start = performance.now();
652
649
  const page = this.requirePage(taskId, "snapshot");
653
650
  if (!page) {
654
651
  return {
@@ -659,51 +656,24 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
659
656
  };
660
657
  }
661
658
 
662
- try {
659
+ return this._logOp("snapshot", { taskId }, async () => {
663
660
  const {
664
661
  snapshot: snapText,
665
662
  elementCount,
666
663
  dialogEvents,
667
664
  } = await this.takeSnapshot(taskId, page);
668
-
669
- this._log("snapshot", {
670
- taskId,
671
- success: true,
672
- elementCount,
673
- dialogEvents: dialogEvents.length,
674
- fingerprint: snapText.slice(0, 16),
675
- time: Math.round(performance.now() - _start),
676
- });
677
-
678
665
  return {
679
666
  success: true,
680
667
  snapshot: snapText,
681
668
  elementCount,
682
669
  dialogEvents,
683
670
  };
684
- } catch (err: unknown) {
685
- this._log("snapshot", {
686
- taskId,
687
- success: false,
688
- elementCount: 0,
689
- error: err instanceof Error ? err.message : String(err),
690
- time: Math.round(performance.now() - _start),
691
- });
692
-
693
- return {
694
- success: false,
695
- snapshot: "",
696
- elementCount: 0,
697
- error: err instanceof Error ? err.message : String(err),
698
- };
699
- }
671
+ }, (r) => ({ elementCount: r.elementCount, dialogEvents: r.dialogEvents.length, fingerprint: r.snapshot.slice(0, 16) }));
700
672
  }
701
673
 
702
674
  // ── Interaction ────────────────────────────────────────────
703
675
 
704
676
  async click(taskId: string, ref: string): Promise<InteractionResult> {
705
- const _start = performance.now();
706
- const phases: Record<string, number> = {};
707
677
  const page = this.requirePage(taskId, "click");
708
678
  if (!page) {
709
679
  return { success: false, error: "No active session" };
@@ -720,7 +690,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
720
690
  name: "(none)",
721
691
  result: "fail",
722
692
  error: `Element ${ref} not found in accessibility tree`,
723
- time: Math.round(performance.now() - _start),
724
693
  });
725
694
  return {
726
695
  success: false,
@@ -737,71 +706,46 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
737
706
  name: node.name,
738
707
  result: "fail",
739
708
  error: `Could not build locator (role: ${node.role})`,
740
- time: Math.round(performance.now() - _start),
741
709
  });
742
710
  return {
743
711
  success: false,
744
712
  error: `Could not build locator for ${ref} (role: ${node.role})`,
745
713
  };
746
714
  }
747
- phases.locate = Math.round(performance.now() - _start);
748
-
749
- try {
750
- const urlBefore = page.url();
751
- await locator.click({ timeout: 5000 });
752
- phases.click = Math.round(performance.now() - _start);
753
-
754
- // Wait for potential navigation to settle (replaces fixed sleep)
755
- const { navigated } = await waitForNavigationSettle(page, urlBefore);
756
- phases.wait = Math.round(performance.now() - _start);
757
-
758
- const newUrl = page.url();
759
- const newTitle = await page.title();
760
- sessionManager.updateSession(taskId, {
761
- currentUrl: newUrl,
762
- currentTitle: newTitle,
763
- });
764
715
 
765
- // Auto-snapshot
766
- const snapResult = await this.takeSnapshot(taskId, page);
767
- phases.snapshot = Math.round(performance.now() - _start);
716
+ return this._logOp("click", { taskId, ref, role: node.role, name: node.name }, async () => {
717
+ try {
718
+ const urlBefore = page.url();
719
+ await locator.click({ timeout: 5000 });
768
720
 
769
- this._log("click", {
770
- taskId,
771
- ref,
772
- role: node.role,
773
- name: node.name,
774
- result: "success",
775
- navigated,
776
- timings: phases,
777
- time: Math.round(performance.now() - _start),
778
- });
721
+ // Wait for potential navigation to settle (replaces fixed sleep)
722
+ const { navigated: _navigated } = await waitForNavigationSettle(page, urlBefore);
779
723
 
780
- return {
781
- success: true,
782
- newUrl,
783
- newTitle,
784
- snapshot: snapResult.snapshot,
785
- elementCount: snapResult.elementCount,
786
- dialogEvents: snapResult.dialogEvents,
787
- };
788
- } catch (err: unknown) {
789
- this._log("click", {
790
- taskId,
791
- ref,
792
- role: node.role,
793
- name: node.name,
794
- result: "fail",
795
- error: err instanceof Error ? err.message : String(err),
796
- timings: phases,
797
- time: Math.round(performance.now() - _start),
798
- });
724
+ const newUrl = page.url();
725
+ const newTitle = await page.title();
726
+ sessionManager.updateSession(taskId, {
727
+ currentUrl: newUrl,
728
+ currentTitle: newTitle,
729
+ });
799
730
 
800
- return {
801
- success: false,
802
- error: `Click failed: ${err instanceof Error ? err.message : String(err)}`,
803
- };
804
- }
731
+ // Auto-snapshot
732
+ const snapResult = await this.takeSnapshot(taskId, page);
733
+
734
+ return {
735
+ success: true,
736
+ newUrl,
737
+ newTitle,
738
+ snapshot: snapResult.snapshot,
739
+ elementCount: snapResult.elementCount,
740
+ dialogEvents: snapResult.dialogEvents,
741
+ };
742
+ } catch (err: unknown) {
743
+ return {
744
+ success: false,
745
+ error: `Click failed: ${err instanceof Error ? err.message : String(err)}`,
746
+ };
747
+ }
748
+ });
805
749
  }
806
750
 
807
751
  async type(
@@ -809,7 +753,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
809
753
  ref: string,
810
754
  text: string,
811
755
  ): Promise<InteractionResult> {
812
- const _start = performance.now();
813
756
  const page = this.requirePage(taskId, "type");
814
757
  if (!page) {
815
758
  return { success: false, error: "No active session" };
@@ -826,7 +769,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
826
769
  name: "(none)",
827
770
  result: "fail",
828
771
  error: `Element ${ref} not found in accessibility tree`,
829
- time: Math.round(performance.now() - _start),
830
772
  });
831
773
  return {
832
774
  success: false,
@@ -843,7 +785,6 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
843
785
  name: node.name,
844
786
  result: "fail",
845
787
  error: `Could not build locator (role: ${node.role})`,
846
- time: Math.round(performance.now() - _start),
847
788
  });
848
789
  return {
849
790
  success: false,
@@ -851,199 +792,141 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
851
792
  };
852
793
  }
853
794
 
854
- try {
855
- await locator.click({ timeout: 5000 }); // Focus first
856
- await locator.fill(text);
857
-
858
- // Auto-snapshot
859
- const snapResult = await this.takeSnapshot(taskId, page);
860
-
861
- this._log("type", {
862
- taskId,
863
- ref,
864
- role: node.role,
865
- name: node.name,
866
- result: "success",
867
- elementCount: snapResult.elementCount,
868
- time: Math.round(performance.now() - _start),
869
- });
870
-
871
- return {
872
- success: true,
873
- snapshot: snapResult.snapshot,
874
- elementCount: snapResult.elementCount,
875
- dialogEvents: snapResult.dialogEvents,
876
- };
877
- } catch (err: unknown) {
878
- this._log("type", {
879
- taskId,
880
- ref,
881
- role: node.role,
882
- name: node.name,
883
- result: "fail",
884
- error: err instanceof Error ? err.message : String(err),
885
- time: Math.round(performance.now() - _start),
886
- });
887
- return {
888
- success: false,
889
- error: `Type failed: ${err instanceof Error ? err.message : String(err)}`,
890
- };
891
- }
795
+ return this._logOp("type", { taskId, ref, role: node.role, name: node.name }, async () => {
796
+ try {
797
+ await locator.click({ timeout: 5000 }); // Focus first
798
+ await locator.fill(text);
799
+
800
+ // Auto-snapshot
801
+ const snapResult = await this.takeSnapshot(taskId, page);
802
+
803
+ return {
804
+ success: true,
805
+ snapshot: snapResult.snapshot,
806
+ elementCount: snapResult.elementCount,
807
+ dialogEvents: snapResult.dialogEvents,
808
+ };
809
+ } catch (err: unknown) {
810
+ return {
811
+ success: false,
812
+ error: `Type failed: ${err instanceof Error ? err.message : String(err)}`,
813
+ };
814
+ }
815
+ }, (r) => ({ elementCount: r.elementCount }));
892
816
  }
893
817
 
894
818
  async scroll(
895
819
  taskId: string,
896
820
  direction: "up" | "down",
897
821
  ): Promise<InteractionResult> {
898
- const _start = performance.now();
899
822
  const page = this.requirePage(taskId, "scroll");
900
823
  if (!page) {
901
824
  return { success: false, error: "No active session" };
902
825
  }
903
826
 
904
- try {
905
- const delta = direction === "down" ? 800 : -800;
906
- await page.evaluate((d: number) => {
907
- window.scrollBy({ top: d, behavior: "smooth" });
908
- }, delta);
909
- await page.waitForTimeout(200);
910
-
911
- const snapResult = await this.takeSnapshot(taskId, page);
912
-
913
- this._log("scroll", {
914
- taskId,
915
- direction,
916
- success: true,
917
- elementCount: snapResult.elementCount,
918
- time: Math.round(performance.now() - _start),
919
- });
920
-
921
- return {
922
- success: true,
923
- snapshot: snapResult.snapshot,
924
- elementCount: snapResult.elementCount,
925
- dialogEvents: snapResult.dialogEvents,
926
- };
927
- } catch (err: unknown) {
928
- this._log("scroll", {
929
- taskId,
930
- direction,
931
- success: false,
932
- error: err instanceof Error ? err.message : String(err),
933
- time: Math.round(performance.now() - _start),
934
- });
935
- return {
936
- success: false,
937
- error: `Scroll failed: ${err instanceof Error ? err.message : String(err)}`,
938
- };
939
- }
827
+ return this._logOp("scroll", { taskId, direction }, async () => {
828
+ try {
829
+ const delta = direction === "down" ? 800 : -800;
830
+ await page.evaluate(
831
+ `window.scrollBy({ top: ${delta}, behavior: "smooth" })`,
832
+ );
833
+ await page.waitForTimeout(200);
834
+
835
+ const snapResult = await this.takeSnapshot(taskId, page);
836
+
837
+ return {
838
+ success: true,
839
+ snapshot: snapResult.snapshot,
840
+ elementCount: snapResult.elementCount,
841
+ dialogEvents: snapResult.dialogEvents,
842
+ };
843
+ } catch (err: unknown) {
844
+ return {
845
+ success: false,
846
+ error: `Scroll failed: ${err instanceof Error ? err.message : String(err)}`,
847
+ };
848
+ }
849
+ }, (r) => ({ elementCount: r.elementCount }));
940
850
  }
941
851
 
942
852
  async goBack(taskId: string): Promise<InteractionResult> {
943
- const _start = performance.now();
944
853
  const page = this.requirePage(taskId, "goBack");
945
854
  if (!page) {
946
855
  return { success: false, error: "No active session" };
947
856
  }
948
857
 
949
- try {
950
- await page.goBack({ waitUntil: "networkidle" });
951
- await page.waitForTimeout(300);
952
-
953
- const newUrl = page.url();
954
- const newTitle = await page.title();
955
- sessionManager.updateSession(taskId, {
956
- currentUrl: newUrl,
957
- currentTitle: newTitle,
958
- });
959
-
960
- const snapResult = await this.takeSnapshot(taskId, page);
961
-
962
- this._log("goBack", {
963
- taskId,
964
- success: true,
965
- elementCount: snapResult.elementCount,
966
- time: Math.round(performance.now() - _start),
967
- });
858
+ return this._logOp("goBack", { taskId }, async () => {
859
+ try {
860
+ await page.goBack({ waitUntil: "networkidle" });
861
+ await page.waitForTimeout(300);
862
+
863
+ const newUrl = page.url();
864
+ const newTitle = await page.title();
865
+ sessionManager.updateSession(taskId, {
866
+ currentUrl: newUrl,
867
+ currentTitle: newTitle,
868
+ });
968
869
 
969
- return {
970
- success: true,
971
- newUrl,
972
- newTitle,
973
- snapshot: snapResult.snapshot,
974
- elementCount: snapResult.elementCount,
975
- dialogEvents: snapResult.dialogEvents,
976
- };
977
- } catch (err: unknown) {
978
- this._log("goBack", {
979
- taskId,
980
- success: false,
981
- error: err instanceof Error ? err.message : String(err),
982
- time: Math.round(performance.now() - _start),
983
- });
984
- return {
985
- success: false,
986
- error: `GoBack failed: ${err instanceof Error ? err.message : String(err)}`,
987
- };
988
- }
870
+ const snapResult = await this.takeSnapshot(taskId, page);
871
+
872
+ return {
873
+ success: true,
874
+ newUrl,
875
+ newTitle,
876
+ snapshot: snapResult.snapshot,
877
+ elementCount: snapResult.elementCount,
878
+ dialogEvents: snapResult.dialogEvents,
879
+ };
880
+ } catch (err: unknown) {
881
+ return {
882
+ success: false,
883
+ error: `GoBack failed: ${err instanceof Error ? err.message : String(err)}`,
884
+ };
885
+ }
886
+ }, (r) => ({ elementCount: r.elementCount }));
989
887
  }
990
888
 
991
889
  async press(taskId: string, key: string): Promise<InteractionResult> {
992
- const _start = performance.now();
993
890
  const page = this.requirePage(taskId, "press");
994
891
  if (!page) {
995
892
  return { success: false, error: "No active session" };
996
893
  }
997
894
 
998
- try {
999
- const urlBefore = page.url();
1000
- await page.keyboard.press(key);
1001
-
1002
- // Wait for potential navigation to settle (replaces fixed sleep).
1003
- // Shorter nav timeout since Enter-on-link nav is typically fast.
1004
- const { navigated } = await waitForNavigationSettle(page, urlBefore, {
1005
- navTimeoutMs: 3000,
1006
- });
1007
-
1008
- const newUrl = page.url();
1009
- const newTitle = await page.title();
1010
- sessionManager.updateSession(taskId, {
1011
- currentUrl: newUrl,
1012
- currentTitle: newTitle,
1013
- });
895
+ return this._logOp("press", { taskId, key }, async () => {
896
+ try {
897
+ const urlBefore = page.url();
898
+ await page.keyboard.press(key);
1014
899
 
1015
- const snapResult = await this.takeSnapshot(taskId, page);
900
+ // Wait for potential navigation to settle (replaces fixed sleep).
901
+ // Shorter nav timeout since Enter-on-link nav is typically fast.
902
+ const { navigated: _navigated } = await waitForNavigationSettle(page, urlBefore, {
903
+ navTimeoutMs: 3000,
904
+ });
1016
905
 
1017
- this._log("press", {
1018
- taskId,
1019
- key,
1020
- success: true,
1021
- navigated,
1022
- elementCount: snapResult.elementCount,
1023
- time: Math.round(performance.now() - _start),
1024
- });
906
+ const newUrl = page.url();
907
+ const newTitle = await page.title();
908
+ sessionManager.updateSession(taskId, {
909
+ currentUrl: newUrl,
910
+ currentTitle: newTitle,
911
+ });
1025
912
 
1026
- return {
1027
- success: true,
1028
- newUrl,
1029
- newTitle,
1030
- snapshot: snapResult.snapshot,
1031
- elementCount: snapResult.elementCount,
1032
- dialogEvents: snapResult.dialogEvents,
1033
- };
1034
- } catch (err: unknown) {
1035
- this._log("press", {
1036
- taskId,
1037
- key,
1038
- success: false,
1039
- error: err instanceof Error ? err.message : String(err),
1040
- time: Math.round(performance.now() - _start),
1041
- });
1042
- return {
1043
- success: false,
1044
- error: `Press failed: ${err instanceof Error ? err.message : String(err)}`,
1045
- };
1046
- }
913
+ const snapResult = await this.takeSnapshot(taskId, page);
914
+
915
+ return {
916
+ success: true,
917
+ newUrl,
918
+ newTitle,
919
+ snapshot: snapResult.snapshot,
920
+ elementCount: snapResult.elementCount,
921
+ dialogEvents: snapResult.dialogEvents,
922
+ };
923
+ } catch (err: unknown) {
924
+ return {
925
+ success: false,
926
+ error: `Press failed: ${err instanceof Error ? err.message : String(err)}`,
927
+ };
928
+ }
929
+ }, (r) => ({ elementCount: r.elementCount }));
1047
930
  }
1048
931
 
1049
932
  // ── Media ──────────────────────────────────────────────────
@@ -1090,7 +973,11 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
1090
973
  clearConsoleLog(taskId);
1091
974
  }
1092
975
 
1093
- async evaluate(taskId: string, expression: string): Promise<EvaluateResult> {
976
+ async evaluate(
977
+ taskId: string,
978
+ expression: string,
979
+ _readOnly?: boolean,
980
+ ): Promise<EvaluateResult> {
1094
981
  const page = this.requirePage(taskId);
1095
982
  if (!page) {
1096
983
  return { success: false, error: "No active session" };
@@ -1110,104 +997,71 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
1110
997
  // ── Cookies & storage state ───────────────────────────────
1111
998
 
1112
999
  async getCookies(taskId: string, urls?: string[]): Promise<CookieResult> {
1113
- const _start = performance.now();
1114
1000
  const entry = this.requireEntry(taskId, "getCookies");
1115
1001
  if (!entry) {
1116
1002
  return { success: false, cookies: [], error: "No active session" };
1117
1003
  }
1118
1004
 
1119
- try {
1120
- const cookies = await entry.context.cookies(urls);
1121
- this._log("getCookies", {
1122
- taskId,
1123
- success: true,
1124
- count: cookies.length,
1125
- time: Math.round(performance.now() - _start),
1126
- });
1127
- return { success: true, cookies };
1128
- } catch (err: unknown) {
1129
- this._log("getCookies", {
1130
- taskId,
1131
- success: false,
1132
- error: err instanceof Error ? err.message : String(err),
1133
- time: Math.round(performance.now() - _start),
1134
- });
1135
- return {
1136
- success: false,
1137
- cookies: [],
1138
- error: err instanceof Error ? err.message : String(err),
1139
- };
1140
- }
1005
+ return this._logOp("getCookies", { taskId }, async () => {
1006
+ try {
1007
+ const cookies = await entry.context.cookies(urls);
1008
+ return { success: true, cookies };
1009
+ } catch (err: unknown) {
1010
+ return {
1011
+ success: false,
1012
+ cookies: [],
1013
+ error: err instanceof Error ? err.message : String(err),
1014
+ };
1015
+ }
1016
+ }, (r) => ({ count: r.cookies.length }));
1141
1017
  }
1142
1018
 
1143
1019
  async addCookies(taskId: string, cookies: Cookie[]): Promise<ResultBase> {
1144
- const _start = performance.now();
1145
1020
  const entry = this.requireEntry(taskId, "addCookies");
1146
1021
  if (!entry) {
1147
1022
  return { success: false, error: "No active session" };
1148
1023
  }
1149
1024
 
1150
- try {
1151
- await entry.context.addCookies(cookies);
1152
- this._log("addCookies", {
1153
- taskId,
1154
- success: true,
1155
- count: cookies.length,
1156
- time: Math.round(performance.now() - _start),
1157
- });
1158
- return { success: true };
1159
- } catch (err: unknown) {
1160
- this._log("addCookies", {
1161
- taskId,
1162
- success: false,
1163
- error: err instanceof Error ? err.message : String(err),
1164
- time: Math.round(performance.now() - _start),
1165
- });
1166
- return {
1167
- success: false,
1168
- error: err instanceof Error ? err.message : String(err),
1169
- };
1170
- }
1025
+ return this._logOp("addCookies", { taskId }, async () => {
1026
+ try {
1027
+ await entry.context.addCookies(cookies);
1028
+ return { success: true };
1029
+ } catch (err: unknown) {
1030
+ return {
1031
+ success: false,
1032
+ error: err instanceof Error ? err.message : String(err),
1033
+ };
1034
+ }
1035
+ });
1171
1036
  }
1172
1037
 
1173
1038
  async clearCookies(
1174
1039
  taskId: string,
1175
1040
  options?: ClearCookiesOptions,
1176
1041
  ): Promise<ResultBase> {
1177
- const _start = performance.now();
1178
1042
  const entry = this.requireEntry(taskId, "clearCookies");
1179
1043
  if (!entry) {
1180
1044
  return { success: false, error: "No active session" };
1181
1045
  }
1182
1046
 
1183
- try {
1184
- await entry.context.clearCookies({
1185
- ...(options?.name ? { name: options.name } : {}),
1186
- ...(options?.domain ? { domain: options.domain } : {}),
1187
- ...(options?.path ? { path: options.path } : {}),
1188
- });
1189
- this._log("clearCookies", {
1190
- taskId,
1191
- success: true,
1192
- time: Math.round(performance.now() - _start),
1193
- });
1194
- return { success: true };
1195
- } catch (err: unknown) {
1196
- this._log("clearCookies", {
1197
- taskId,
1198
- success: false,
1199
- error: err instanceof Error ? err.message : String(err),
1200
- time: Math.round(performance.now() - _start),
1201
- });
1202
- return {
1203
- success: false,
1204
- error: err instanceof Error ? err.message : String(err),
1205
- };
1206
- }
1047
+ return this._logOp("clearCookies", { taskId }, async () => {
1048
+ try {
1049
+ await entry.context.clearCookies({
1050
+ ...(options?.name ? { name: options.name } : {}),
1051
+ ...(options?.domain ? { domain: options.domain } : {}),
1052
+ ...(options?.path ? { path: options.path } : {}),
1053
+ });
1054
+ return { success: true };
1055
+ } catch (err: unknown) {
1056
+ return {
1057
+ success: false,
1058
+ error: err instanceof Error ? err.message : String(err),
1059
+ };
1060
+ }
1061
+ });
1207
1062
  }
1208
1063
 
1209
1064
  async getStorageState(taskId: string): Promise<StorageStateResult> {
1210
- const _start = performance.now();
1211
1065
  const entry = this.requireEntry(taskId, "getStorageState");
1212
1066
  if (!entry) {
1213
1067
  return {
@@ -1218,34 +1072,23 @@ export abstract class PlaywrightPluginBase implements BrowserPlugin {
1218
1072
  };
1219
1073
  }
1220
1074
 
1221
- try {
1222
- const state = await entry.context.storageState();
1223
- this._log("getStorageState", {
1224
- taskId,
1225
- success: true,
1226
- cookies: state.cookies.length,
1227
- origins: state.origins.length,
1228
- time: Math.round(performance.now() - _start),
1229
- });
1230
- return {
1231
- success: true,
1232
- cookies: state.cookies,
1233
- origins: state.origins,
1234
- };
1235
- } catch (err: unknown) {
1236
- this._log("getStorageState", {
1237
- taskId,
1238
- success: false,
1239
- error: err instanceof Error ? err.message : String(err),
1240
- time: Math.round(performance.now() - _start),
1241
- });
1242
- return {
1243
- success: false,
1244
- cookies: [],
1245
- origins: [],
1246
- error: err instanceof Error ? err.message : String(err),
1247
- };
1248
- }
1075
+ return this._logOp("getStorageState", { taskId }, async () => {
1076
+ try {
1077
+ const state = await entry.context.storageState();
1078
+ return {
1079
+ success: true,
1080
+ cookies: state.cookies,
1081
+ origins: state.origins,
1082
+ };
1083
+ } catch (err: unknown) {
1084
+ return {
1085
+ success: false,
1086
+ cookies: [],
1087
+ origins: [],
1088
+ error: err instanceof Error ? err.message : String(err),
1089
+ };
1090
+ }
1091
+ }, (r) => ({ cookies: r.cookies.length, origins: r.origins.length }));
1249
1092
  }
1250
1093
 
1251
1094
  // ── Per-task cleanup ───────────────────────────────────────