ai-dev-browser 0.8.2__tar.gz → 0.8.4__tar.gz

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 (181) hide show
  1. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/PKG-INFO +1 -1
  2. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/_cli.py +87 -26
  3. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_tab.py +4 -0
  4. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_transport.py +30 -0
  5. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/ax.py +8 -2
  6. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/elements.py +63 -9
  7. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/page.py +133 -13
  8. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/PKG-INFO +1 -1
  9. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.github/workflows/ci.yml +0 -0
  10. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.github/workflows/publish.yml +0 -0
  11. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.gitignore +0 -0
  12. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.gitmodules +0 -0
  13. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.pre-commit-config.yaml +0 -0
  14. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/.secrets.baseline +0 -0
  15. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/LICENSE +0 -0
  16. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/README.md +0 -0
  17. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/__init__.py +0 -0
  18. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/_version.py +0 -0
  19. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/__init__.py +0 -0
  20. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/accessibility.py +0 -0
  21. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/animation.py +0 -0
  22. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/audits.py +0 -0
  23. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/autofill.py +0 -0
  24. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/background_service.py +0 -0
  25. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/bluetooth_emulation.py +0 -0
  26. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/browser.py +0 -0
  27. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/cache_storage.py +0 -0
  28. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/cast.py +0 -0
  29. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/console.py +0 -0
  30. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/crash_report_context.py +0 -0
  31. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/css.py +0 -0
  32. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/debugger.py +0 -0
  33. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/device_access.py +0 -0
  34. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/device_orientation.py +0 -0
  35. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom.py +0 -0
  36. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_debugger.py +0 -0
  37. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_snapshot.py +0 -0
  38. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/dom_storage.py +0 -0
  39. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/emulation.py +0 -0
  40. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/event_breakpoints.py +0 -0
  41. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/extensions.py +0 -0
  42. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/fed_cm.py +0 -0
  43. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/fetch.py +0 -0
  44. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/file_system.py +0 -0
  45. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/headless_experimental.py +0 -0
  46. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/heap_profiler.py +0 -0
  47. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/indexed_db.py +0 -0
  48. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/input_.py +0 -0
  49. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/inspector.py +0 -0
  50. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/io.py +0 -0
  51. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/layer_tree.py +0 -0
  52. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/log.py +0 -0
  53. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/media.py +0 -0
  54. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/memory.py +0 -0
  55. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/network.py +0 -0
  56. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/overlay.py +0 -0
  57. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/page.py +0 -0
  58. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/performance.py +0 -0
  59. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/performance_timeline.py +0 -0
  60. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/preload.py +0 -0
  61. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/profiler.py +0 -0
  62. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/pwa.py +0 -0
  63. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/py.typed +0 -0
  64. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/runtime.py +0 -0
  65. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/schema.py +0 -0
  66. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/security.py +0 -0
  67. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/service_worker.py +0 -0
  68. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/smart_card_emulation.py +0 -0
  69. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/storage.py +0 -0
  70. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/system_info.py +0 -0
  71. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/target.py +0 -0
  72. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/tethering.py +0 -0
  73. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/tracing.py +0 -0
  74. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/util.py +0 -0
  75. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_audio.py +0 -0
  76. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_authn.py +0 -0
  77. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/cdp/web_mcp.py +0 -0
  78. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/__init__.py +0 -0
  79. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_case.py +0 -0
  80. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/_element.py +0 -0
  81. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/browser.py +0 -0
  82. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/cdp.py +0 -0
  83. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/chrome.py +0 -0
  84. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/config.py +0 -0
  85. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/connection.py +0 -0
  86. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/cookies.py +0 -0
  87. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/dialog.py +0 -0
  88. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/download.py +0 -0
  89. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/human.py +0 -0
  90. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/login.py +0 -0
  91. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/mouse.py +0 -0
  92. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/navigation.py +0 -0
  93. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/overlays.py +0 -0
  94. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/port.py +0 -0
  95. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/process.py +0 -0
  96. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/snapshot.py +0 -0
  97. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/storage.py +0 -0
  98. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/tabs.py +0 -0
  99. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/text_match.py +0 -0
  100. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/core/window.py +0 -0
  101. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/__init__.py +0 -0
  102. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/job.py +0 -0
  103. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/persistence.py +0 -0
  104. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/pool.py +0 -0
  105. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/pool/worker.py +0 -0
  106. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/profile.py +0 -0
  107. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/py.typed +0 -0
  108. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/__init__.py +0 -0
  109. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/_generate.py +0 -0
  110. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_list.py +0 -0
  111. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_start.py +0 -0
  112. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/browser_stop.py +0 -0
  113. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cdp_send.py +0 -0
  114. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_html_id.py +0 -0
  115. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_ref.py +0 -0
  116. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_text.py +0 -0
  117. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/click_by_xpath.py +0 -0
  118. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_list.py +0 -0
  119. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_load.py +0 -0
  120. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/cookies_save.py +0 -0
  121. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/dialog_respond.py +0 -0
  122. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/download.py +0 -0
  123. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/drag_by_ref.py +0 -0
  124. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_html_id.py +0 -0
  125. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_text.py +0 -0
  126. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/find_by_xpath.py +0 -0
  127. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/focus_by_ref.py +0 -0
  128. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/highlight_by_ref.py +0 -0
  129. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/hover_by_ref.py +0 -0
  130. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/html_by_ref.py +0 -0
  131. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/js_evaluate.py +0 -0
  132. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/login_interactive.py +0 -0
  133. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_click.py +0 -0
  134. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_drag.py +0 -0
  135. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/mouse_move.py +0 -0
  136. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_discover.py +0 -0
  137. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_emulate_focus.py +0 -0
  138. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_goto.py +0 -0
  139. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_html.py +0 -0
  140. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_info.py +0 -0
  141. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_reload.py +0 -0
  142. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_screenshot.py +0 -0
  143. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_scroll.py +0 -0
  144. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_element.py +0 -0
  145. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_ready.py +0 -0
  146. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/page_wait_url.py +0 -0
  147. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/screenshot_by_ref.py +0 -0
  148. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/select_by_ref.py +0 -0
  149. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/storage_get.py +0 -0
  150. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/storage_set.py +0 -0
  151. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_close.py +0 -0
  152. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_list.py +0 -0
  153. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_new.py +0 -0
  154. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/tab_switch.py +0 -0
  155. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/type_by_ref.py +0 -0
  156. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/type_by_text.py +0 -0
  157. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/upload_by_ref.py +0 -0
  158. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser/tools/window_set.py +0 -0
  159. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/SOURCES.txt +0 -0
  160. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/dependency_links.txt +0 -0
  161. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/requires.txt +0 -0
  162. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/ai_dev_browser.egg-info/top_level.txt +0 -0
  163. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/pyproject.toml +0 -0
  164. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/scripts/sync_cdp.py +0 -0
  165. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/setup.cfg +0 -0
  166. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/__init__.py +0 -0
  167. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/conftest.py +0 -0
  168. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/__init__.py +0 -0
  169. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/scenarios_workspace.json +0 -0
  170. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_dialog_respond.py +0 -0
  171. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_find_and_interact_workflows.py +0 -0
  172. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_locator_workflows.py +0 -0
  173. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_multi_profile_pool.py +0 -0
  174. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_page_reload.py +0 -0
  175. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_startup_timeout.py +0 -0
  176. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_timeout_and_retry.py +0 -0
  177. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/integration/test_workspace_workflows.py +0 -0
  178. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_case.py +0 -0
  179. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_cli_param_type.py +0 -0
  180. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/tests/unit/test_no_acronym_rot.py +0 -0
  181. {ai_dev_browser-0.8.2 → ai_dev_browser-0.8.4}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-browser
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: A browser for AI to develop web automation — human-like automation that works seamlessly in a world designed for humans
5
5
  Author: sudoprivacy
6
6
  License: AGPL-3.0
@@ -88,6 +88,48 @@ def _parse_docstring_args(docstring: str) -> dict[str, str]:
88
88
  return args_section
89
89
 
90
90
 
91
+ def _parse_docstring_failure(docstring: str) -> str | None:
92
+ """Extract the body of the Failure: section from a Google-style docstring.
93
+
94
+ Tool authors write failure-path steering once in the docstring's
95
+ `Failure:` section (parallel to `Args:` / `Returns:`). This parser
96
+ extracts it at wrap time; `wrap_core` then auto-injects it as the
97
+ `hint` field on any failure return — SSOT with auto-split across
98
+ `--help` (full docstring, reference surface) and failure `hint`
99
+ (just this section, runtime steering surface).
100
+
101
+ Rationale: guidance about "what to do if this tool fails" placed
102
+ only in the docstring reaches the LLM at most via `--help`, which
103
+ is an on-demand call the LLM rarely makes at invocation-failure
104
+ time. Routing the same authored text into the failure return is
105
+ the only channel with 100% reach at the moment the LLM needs to
106
+ recover.
107
+
108
+ Returns the flat concatenated hint text, or None if no Failure:
109
+ section is present.
110
+ """
111
+ if not docstring:
112
+ return None
113
+
114
+ in_failure = False
115
+ lines: list[str] = []
116
+ for raw in docstring.split("\n"):
117
+ stripped = raw.strip()
118
+ # Google-style section headers are single-word lines ending with ':'
119
+ is_section_header = stripped.endswith(":") and len(stripped.split()) == 1
120
+ if is_section_header:
121
+ if stripped == "Failure:":
122
+ in_failure = True
123
+ continue
124
+ if in_failure:
125
+ break # next section ends the Failure block
126
+ elif in_failure:
127
+ lines.append(stripped)
128
+
129
+ text = " ".join(line for line in lines if line).strip()
130
+ return text or None
131
+
132
+
91
133
  def _get_param_type(hint) -> type | Callable[[str], bool]:
92
134
  """Convert type hint to argparse type."""
93
135
  import types
@@ -368,25 +410,38 @@ def wrap_core(core_func: Callable, result_key: str = "success") -> Callable:
368
410
  element_click = as_cli()(wrap_core(click, "clicked"))
369
411
  """
370
412
 
413
+ failure_hint = _parse_docstring_failure(core_func.__doc__ or "")
414
+
371
415
  @functools.wraps(core_func)
372
416
  async def wrapper(*args, **kwargs):
373
417
  try:
374
418
  result = await core_func(*args, **kwargs)
375
- if isinstance(result, bool):
376
- if result:
377
- return {result_key: True}
378
- else:
379
- return {"error": "Operation failed"}
380
- elif isinstance(result, dict):
381
- # Filter out non-serializable values for CLI output
382
- return _filter_dict_for_json(result)
383
- else:
384
- return {result_key: result}
385
419
  except Exception as e:
386
420
  # Verbatim message — Python `repr(e)` and CLI stdout stay in
387
- # lockstep (cli-args-ssot rule 6: never re-render error text
421
+ # lockstep (cli-args-ssot rule 7: never re-render error text
388
422
  # in tool files).
389
- return {"error": str(e)}
423
+ out: dict = {"error": str(e)}
424
+ if failure_hint:
425
+ out["hint"] = failure_hint
426
+ return out
427
+
428
+ if isinstance(result, bool):
429
+ if result:
430
+ return {result_key: True}
431
+ out = {"error": "Operation failed"}
432
+ if failure_hint:
433
+ out["hint"] = failure_hint
434
+ return out
435
+ if isinstance(result, dict):
436
+ filtered = _filter_dict_for_json(result)
437
+ # Auto-inject failure hint when the tool reports failure via
438
+ # result_key=False. Pairs with cli-args-ssot Rule 5a: failure
439
+ # steering goes through the return channel (100% reach at
440
+ # invocation time), not docstring (only reaches on --help).
441
+ if filtered.get(result_key) is False and failure_hint:
442
+ filtered.setdefault("hint", failure_hint)
443
+ return filtered
444
+ return {result_key: result}
390
445
 
391
446
  return wrapper
392
447
 
@@ -411,24 +466,30 @@ def wrap_core_sync(core_func: Callable, result_key: str = "success") -> Callable
411
466
  browser_start = as_cli(requires_tab=False)(wrap_core_sync(browser_start, "port"))
412
467
  """
413
468
 
469
+ failure_hint = _parse_docstring_failure(core_func.__doc__ or "")
470
+
414
471
  @functools.wraps(core_func)
415
472
  def wrapper(*args, **kwargs):
416
473
  try:
417
474
  result = core_func(*args, **kwargs)
418
- if isinstance(result, bool):
419
- if result:
420
- return {result_key: True}
421
- else:
422
- return {"error": "Operation failed"}
423
- elif isinstance(result, dict):
424
- # Filter out non-serializable values for CLI output
425
- return _filter_dict_for_json(result)
426
- else:
427
- return {result_key: result}
428
475
  except Exception as e:
429
- # Verbatim message Python `repr(e)` and CLI stdout stay in
430
- # lockstep (cli-args-ssot rule 6: never re-render error text
431
- # in tool files).
432
- return {"error": str(e)}
476
+ out: dict = {"error": str(e)}
477
+ if failure_hint:
478
+ out["hint"] = failure_hint
479
+ return out
480
+
481
+ if isinstance(result, bool):
482
+ if result:
483
+ return {result_key: True}
484
+ out = {"error": "Operation failed"}
485
+ if failure_hint:
486
+ out["hint"] = failure_hint
487
+ return out
488
+ if isinstance(result, dict):
489
+ filtered = _filter_dict_for_json(result)
490
+ if filtered.get(result_key) is False and failure_hint:
491
+ filtered.setdefault("hint", failure_hint)
492
+ return filtered
493
+ return {result_key: result}
433
494
 
434
495
  return wrapper
@@ -195,6 +195,10 @@ class Tab:
195
195
  """Register a CDP event handler."""
196
196
  self._connection.add_handler(event_type, handler)
197
197
 
198
+ def remove_handler(self, event_type, handler) -> bool:
199
+ """Unregister a CDP event handler (pair with add_handler)."""
200
+ return self._connection.remove_handler(event_type, handler)
201
+
198
202
  # =========================================================================
199
203
  # JavaScript evaluation
200
204
  # =========================================================================
@@ -242,6 +242,36 @@ class CDPConnection:
242
242
  else:
243
243
  self.handlers[evt].append(handler)
244
244
 
245
+ def remove_handler(
246
+ self,
247
+ event_type: type,
248
+ handler: Callable,
249
+ ) -> bool:
250
+ """Unregister a previously-added event handler.
251
+
252
+ Counterpart to add_handler for short-lived captures (e.g.
253
+ `js_evaluate` subscribing to `Runtime.consoleAPICalled` for the
254
+ duration of a single eval then detaching). Without this, each
255
+ call leaks the handler closure and its captured state.
256
+
257
+ Args:
258
+ event_type: CDP event class the handler was registered on.
259
+ Only single-class removal is supported; to undo a
260
+ module-level add_handler, unregister per-class.
261
+ handler: The exact callable passed to add_handler.
262
+
263
+ Returns:
264
+ True if removed, False if not found.
265
+ """
266
+ bucket = self.handlers.get(event_type)
267
+ if not bucket:
268
+ return False
269
+ try:
270
+ bucket.remove(handler)
271
+ return True
272
+ except ValueError:
273
+ return False
274
+
245
275
  # Domains that should never be removed by _register_handlers cleanup.
246
276
  # These are either always-on (target, storage, input_) or essential
247
277
  # for core operations (page, dom) — enabled by Tab._ensure_connected().
@@ -309,8 +309,14 @@ async def click_by_ref(
309
309
  dict with clicked status, element info, and navigation feedback:
310
310
  `{clicked, ref, role, name, url_before, url_after, title_after, navigated}`.
311
311
  `navigated=True` means the top-level URL changed after the click
312
- (SPA route change or full page load). Use this to confirm the click
313
- had the intended side effect instead of chaining a screenshot + discover.
312
+ (SPA route change or full page load).
313
+
314
+ Failure:
315
+ Ref is stale (page navigated or element was removed between
316
+ the `page_discover` / `find_by_text` call that returned it
317
+ and this click). Re-run `page_discover` or `find_by_text` to
318
+ get a fresh ref, or use a stable locator — `click_by_html_id`
319
+ / `click_by_xpath` / `click_by_text`.
314
320
 
315
321
  Example:
316
322
  # First page_discover elements
@@ -432,6 +432,15 @@ async def page_wait_element(
432
432
 
433
433
  Returns:
434
434
  dict with found, elapsed, message
435
+
436
+ Failure:
437
+ Element didn't appear within `timeout` seconds. Try a longer
438
+ timeout if the page is slow; a broader locator (partial text
439
+ instead of exact, less-specific CSS selector); or confirm the
440
+ element is expected on this page via `page_discover`. For
441
+ iframe-embedded targets with a text locator, note that
442
+ text-based wait scans the top frame only — use
443
+ `find_by_text` + `click_by_ref` pattern instead of waiting.
435
444
  """
436
445
  result = await _wait_for_element(tab, text=text, selector=selector, timeout=timeout)
437
446
 
@@ -464,13 +473,6 @@ async def click_by_text(
464
473
  (form-validation error rendering, captcha pixels for OCR, final
465
474
  result view for the user).
466
475
 
467
- **Top-frame only.** Unlike `click_by_html_id` / `click_by_xpath`
468
- (which recurse through `window.frames`) and `find_by_text` (which
469
- scans the full AX tree), this goes through CDP `DOM.performSearch`
470
- and won't find text inside an iframe (nav menus in `<iframe>`,
471
- embedded widgets). For those, use `find_by_text` → `click_by_ref`
472
- or `click_by_xpath` / `click_by_html_id`.
473
-
474
476
  Prefer when text is unique / unambiguous and top-frame. For elements
475
477
  you already identified via `page_discover`, use `click_by_ref`.
476
478
 
@@ -483,8 +485,17 @@ async def click_by_text(
483
485
  Returns:
484
486
  dict with clicked, text, url_before, url_after, title_after, navigated.
485
487
  `navigated=True` means the top-level URL changed after the click
486
- (SPA route change or full page load). Use this to confirm the click
487
- had the intended side effect instead of chaining a screenshot + discover.
488
+ (SPA route change or full page load).
489
+
490
+ Failure:
491
+ Not found in the top frame. This tool uses CDP
492
+ `DOM.performSearch` on the main document only — it won't find
493
+ text inside iframes (nav menus in `<iframe>`, embedded
494
+ widgets). Try `find_by_text` → `click_by_ref` (scans
495
+ same-origin iframes and falls back to non-interactable AX
496
+ nodes like `<div onclick>`), or `click_by_xpath` /
497
+ `click_by_html_id` which recurse through `window.frames`
498
+ natively.
488
499
 
489
500
  Example:
490
501
  click_by_text("登录")
@@ -551,6 +562,15 @@ async def find_by_text(
551
562
  Returns:
552
563
  dict: `{found: True, ref, role, name, x, y, ...}` on hit,
553
564
  `{found: False, text}` otherwise.
565
+
566
+ Failure:
567
+ Text not found in main frame or any same-origin iframe, in
568
+ either the interactable or fallback tier. Check spelling /
569
+ case (match is case-insensitive substring); try a shorter
570
+ substring; or switch locator — `find_by_html_id` /
571
+ `find_by_xpath` if a DOM-level locator is known. For a broad
572
+ survey of what's on the page, run `page_discover` without a
573
+ text filter. Cross-origin iframes are not scanned.
554
574
  """
555
575
  from .snapshot import page_discover
556
576
 
@@ -606,6 +626,13 @@ async def type_by_text(
606
626
  Returns:
607
627
  dict with typed status
608
628
 
629
+ Failure:
630
+ Input with this accessible name not found in the top frame.
631
+ For iframe-embedded inputs, use `find_by_text` (AX-tree scan,
632
+ iframe-aware) to get a ref, then `type_by_ref`. If the input
633
+ lacks an accessible name, locate by html id with
634
+ `find_by_html_id` → `type_by_ref`.
635
+
609
636
  Example:
610
637
  type_by_text(name="用户名", text="myusername")
611
638
  type_by_text(name="Search", text="query", clear=True)
@@ -822,6 +849,12 @@ async def find_by_html_id(tab: Tab, html_id: str) -> dict:
822
849
  dict: `{found: true, tag, text, visible, attrs}` on hit,
823
850
  `{found: false}` otherwise.
824
851
 
852
+ Failure:
853
+ No element with this id in any same-origin frame. Use
854
+ `page_discover` to see the ids actually present, or switch
855
+ locator — `find_by_text` if you know the visible label,
856
+ `find_by_xpath` for attribute-predicate / positional lookups.
857
+
825
858
  Example:
826
859
  result = await find_by_html_id(tab, "submit-btn")
827
860
  if result["found"] and result["visible"]:
@@ -869,6 +902,13 @@ async def click_by_html_id(tab: Tab, html_id: str) -> dict:
869
902
  Returns:
870
903
  dict: `{clicked, html_id, url_before, url_after, title_after, navigated, error?}`.
871
904
  `navigated=True` means the top-level URL changed after the click.
905
+
906
+ Failure:
907
+ No element with this id found in any same-origin frame. Run
908
+ `find_by_html_id` first to verify existence and get attrs, or
909
+ switch locator — `click_by_text` (top frame only, use
910
+ `find_by_text` → `click_by_ref` for iframe targets) or
911
+ `click_by_xpath` for attribute / positional predicates.
872
912
  """
873
913
  url_before_state = await _capture_page_state(tab)
874
914
  url_before = url_before_state.get("url", "")
@@ -927,6 +967,14 @@ async def find_by_xpath(tab: Tab, xpath: str) -> dict:
927
967
  Returns:
928
968
  dict: `{found: true, tag, text, visible, attrs}` on hit,
929
969
  `{found: false}` otherwise.
970
+
971
+ Failure:
972
+ XPath returned no match in any same-origin frame. Shorten the
973
+ expression (e.g. `//button` instead of
974
+ `//button[@data-role='x']`) to verify the broader target
975
+ exists, or switch locator — `find_by_text` by visible label,
976
+ `find_by_html_id` if an id is known, or `page_discover` for a
977
+ structural survey.
930
978
  """
931
979
  expr = """
932
980
  (function(xpath) {
@@ -975,6 +1023,12 @@ async def click_by_xpath(tab: Tab, xpath: str) -> dict:
975
1023
 
976
1024
  Returns:
977
1025
  dict: `{clicked, xpath, url_before, url_after, title_after, navigated, error?}`.
1026
+
1027
+ Failure:
1028
+ XPath returned no match in any same-origin frame. Verify with
1029
+ `find_by_xpath` first (returns the matched element's attrs
1030
+ without clicking), or switch locator — `click_by_text` /
1031
+ `click_by_html_id`, or `page_discover` → `click_by_ref`.
978
1032
  """
979
1033
  url_before_state = await _capture_page_state(tab)
980
1034
  url_before = url_before_state.get("url", "")
@@ -62,12 +62,14 @@ def read_screenshot_metadata(path: str) -> dict:
62
62
 
63
63
 
64
64
  async def js_evaluate(tab: Tab, expression: str) -> dict:
65
- """Use when: NO specific tool fits — this is the last-resort raw JS
66
- escape hatch. Returns `{result}` with whatever the expression evaluated
67
- to (or `{error}`). Next step is interpreting that value yourself.
65
+ """Use when: NO specific tool fits — the last-resort raw JS escape
66
+ hatch. Works equally for **read** expressions (`document.title`,
67
+ `innerText`) and **side-effect** expressions (`.click()`, `.submit()`,
68
+ DOM mutations) — the return dict captures *everything* observable
69
+ during the eval, not just the expression value.
68
70
 
69
- Before picking this: the locate+act combinations below cover almost all
70
- intents atomically (and give you navigation feedback on clicks):
71
+ Before picking this: the locate+act combinations below cover almost
72
+ all intents atomically and are more specific:
71
73
 
72
74
  - Locate + act by html id: `click_by_html_id` / `find_by_html_id`
73
75
  - Locate + act by XPath: `click_by_xpath` / `find_by_xpath`
@@ -85,18 +87,136 @@ async def js_evaluate(tab: Tab, expression: str) -> dict:
85
87
 
86
88
  Args:
87
89
  tab: Tab instance
88
- expression: JavaScript code to execute. Result of last expression is returned.
90
+ expression: JavaScript code to execute. Result of last expression
91
+ is returned. `console.log` / `warn` / `error` / `info` output
92
+ during the eval is captured separately.
89
93
 
90
94
  Returns:
91
- dict with result (serializable) or error
95
+ dict with:
96
+
97
+ - `result`: the expression's evaluated value (may be `None` if
98
+ the expression is a void side-effect like `.click()`)
99
+ - `console`: list of `{level, text}` entries emitted during
100
+ the eval, if any (only present when non-empty)
101
+ - `url_before` / `url_after` / `title_after` / `navigated`:
102
+ top-level navigation observables, same shape as
103
+ `click_by_*`. `navigated=True` iff URL changed.
104
+ - `error`: `{message, description, stack}` if the expression
105
+ threw; absent on success.
106
+
107
+ Example:
108
+ # Read — result field carries the answer
109
+ await js_evaluate(tab, "document.title")
110
+ # → {"result": "...", "url_before": ..., "navigated": False}
111
+
112
+ # Side effect — navigated/url_after + console confirm what happened
113
+ await js_evaluate(tab, "document.querySelector('#login').click()")
114
+ # → {"result": None, "url_before": "/login", "url_after": "/home",
115
+ # "navigated": True, "title_after": "Home"}
116
+
117
+ # Debugging — console lines captured
118
+ await js_evaluate(tab, "console.log('x=', x); x + 1")
119
+ # → {"result": 2, "console": [{"level": "log", "text": "x= 1"}], ...}
92
120
  """
93
- result = await tab.evaluate(expression)
94
- # Try to serialize result
121
+ from ai_dev_browser.cdp import runtime
122
+
123
+ from .elements import _POST_CLICK_NAV_DELAY, _capture_page_state
124
+
125
+ before = await _capture_page_state(tab)
126
+
127
+ console_msgs: list[dict] = []
128
+
129
+ def _stringify_arg(arg) -> str:
130
+ # Runtime.RemoteObject: value (primitive) / description / unserializable_value
131
+ val = getattr(arg, "value", None)
132
+ if val is not None:
133
+ try:
134
+ return json.dumps(val, ensure_ascii=False)
135
+ except (TypeError, ValueError):
136
+ return str(val)
137
+ desc = getattr(arg, "description", None)
138
+ if desc:
139
+ return str(desc)
140
+ unser = getattr(arg, "unserializable_value", None)
141
+ if unser:
142
+ return str(unser)
143
+ return ""
144
+
145
+ def on_console(event):
146
+ level = getattr(event, "type_", "log")
147
+ text = " ".join(_stringify_arg(a) for a in event.args)
148
+ console_msgs.append({"level": level, "text": text})
149
+
150
+ # Runtime.enable() is idempotent and required for consoleAPICalled events.
151
+ await tab.send(runtime.enable())
152
+ tab.add_handler(runtime.ConsoleAPICalled, on_console)
153
+
154
+ result_value: object = None
155
+ error_info: dict | None = None
95
156
  try:
96
- json.dumps(result)
97
- return {"result": result}
98
- except (TypeError, ValueError):
99
- return {"result": str(result)}
157
+ raw = await tab.evaluate(expression)
158
+ except Exception as e:
159
+ error_info = {"message": str(e)}
160
+ else:
161
+ # tab.evaluate returns ExceptionDetails on eval-time exceptions
162
+ # (not a Python raise), see _tab.py::evaluate.
163
+ if isinstance(raw, runtime.ExceptionDetails):
164
+ exc = getattr(raw, "exception", None)
165
+ stack = getattr(raw, "stack_trace", None)
166
+ error_info = {
167
+ "message": getattr(raw, "text", "") or "",
168
+ "description": getattr(exc, "description", None) if exc else None,
169
+ "stack": [
170
+ {
171
+ "function": f.function_name,
172
+ "url": f.url,
173
+ "line": f.line_number,
174
+ "column": f.column_number,
175
+ }
176
+ for f in (stack.call_frames if stack else [])
177
+ ]
178
+ if stack
179
+ else None,
180
+ }
181
+ else:
182
+ try:
183
+ json.dumps(raw)
184
+ result_value = raw
185
+ except (TypeError, ValueError):
186
+ result_value = str(raw)
187
+
188
+ # Give any navigation triggered by the eval a moment to commit,
189
+ # mirroring click_by_*'s _POST_CLICK_NAV_DELAY so URL snapshots
190
+ # reflect the post-action state.
191
+ import asyncio as _asyncio
192
+
193
+ await _asyncio.sleep(_POST_CLICK_NAV_DELAY)
194
+
195
+ try:
196
+ after = await _capture_page_state(tab)
197
+ except Exception:
198
+ # Context destroyed mid-read (full-page nav) — we know URL changed
199
+ after = {"url": None, "title": None}
200
+
201
+ tab.remove_handler(runtime.ConsoleAPICalled, on_console)
202
+
203
+ url_before = before.get("url", "") if isinstance(before, dict) else ""
204
+ url_after = after.get("url") if isinstance(after, dict) else None
205
+ title_after = after.get("title", "") if isinstance(after, dict) else ""
206
+ navigated = bool(url_before) and url_after is not None and url_after != url_before
207
+
208
+ out: dict = {
209
+ "result": result_value,
210
+ "url_before": url_before,
211
+ "url_after": url_after,
212
+ "title_after": title_after,
213
+ "navigated": navigated,
214
+ }
215
+ if console_msgs:
216
+ out["console"] = console_msgs
217
+ if error_info is not None:
218
+ out["error"] = error_info
219
+ return out
100
220
 
101
221
 
102
222
  async def page_screenshot(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ai-dev-browser
3
- Version: 0.8.2
3
+ Version: 0.8.4
4
4
  Summary: A browser for AI to develop web automation — human-like automation that works seamlessly in a world designed for humans
5
5
  Author: sudoprivacy
6
6
  License: AGPL-3.0
File without changes
File without changes