cmdop 0.1.17__tar.gz → 0.1.18__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 (201) hide show
  1. {cmdop-0.1.17 → cmdop-0.1.18}/PKG-INFO +30 -23
  2. {cmdop-0.1.17 → cmdop-0.1.18}/README.md +29 -22
  3. {cmdop-0.1.17 → cmdop-0.1.18}/pyproject.toml +1 -1
  4. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/__init__.py +1 -1
  5. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/session.py +115 -1
  6. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/session.py +47 -0
  7. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/__init__.py +2 -0
  8. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/interaction.py +28 -0
  9. cmdop-0.1.18/src/cmdop/services/browser/js/scroll.py +224 -0
  10. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/session.py +58 -23
  11. cmdop-0.1.17/src/cmdop/services/browser/js/scroll.py +0 -133
  12. {cmdop-0.1.17 → cmdop-0.1.18}/.gitignore +0 -0
  13. {cmdop-0.1.17 → cmdop-0.1.18}/LICENSE +0 -0
  14. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/__init__.py +0 -0
  15. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.py +0 -0
  16. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.pyi +0 -0
  17. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2_grpc.py +0 -0
  18. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.py +0 -0
  19. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.pyi +0 -0
  20. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2_grpc.py +0 -0
  21. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.py +0 -0
  22. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.pyi +0 -0
  23. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2_grpc.py +0 -0
  24. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/__init__.py +0 -0
  25. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.py +0 -0
  26. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.pyi +0 -0
  27. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2_grpc.py +0 -0
  28. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.py +0 -0
  29. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.pyi +0 -0
  30. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2_grpc.py +0 -0
  31. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.py +0 -0
  32. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.pyi +0 -0
  33. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2_grpc.py +0 -0
  34. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.py +0 -0
  35. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.pyi +0 -0
  36. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2_grpc.py +0 -0
  37. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.py +0 -0
  38. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.pyi +0 -0
  39. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2_grpc.py +0 -0
  40. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.py +0 -0
  41. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.pyi +0 -0
  42. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2_grpc.py +0 -0
  43. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.py +0 -0
  44. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.pyi +0 -0
  45. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2_grpc.py +0 -0
  46. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.py +0 -0
  47. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.pyi +0 -0
  48. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2_grpc.py +0 -0
  49. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.py +0 -0
  50. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.pyi +0 -0
  51. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2_grpc.py +0 -0
  52. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.py +0 -0
  53. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.pyi +0 -0
  54. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2_grpc.py +0 -0
  55. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/__init__.py +0 -0
  56. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.py +0 -0
  57. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.pyi +0 -0
  58. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2_grpc.py +0 -0
  59. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.py +0 -0
  60. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.pyi +0 -0
  61. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2_grpc.py +0 -0
  62. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.py +0 -0
  63. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.pyi +0 -0
  64. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2_grpc.py +0 -0
  65. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.py +0 -0
  66. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.pyi +0 -0
  67. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2_grpc.py +0 -0
  68. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.py +0 -0
  69. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.pyi +0 -0
  70. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2_grpc.py +0 -0
  71. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.py +0 -0
  72. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.pyi +0 -0
  73. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2_grpc.py +0 -0
  74. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/message_pool.py +0 -0
  75. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/py.typed +0 -0
  76. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/__init__.py +0 -0
  77. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.py +0 -0
  78. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.pyi +0 -0
  79. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2_grpc.py +0 -0
  80. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.py +0 -0
  81. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.pyi +0 -0
  82. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2_grpc.py +0 -0
  83. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.py +0 -0
  84. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.pyi +0 -0
  85. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2_grpc.py +0 -0
  86. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.py +0 -0
  87. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.pyi +0 -0
  88. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2_grpc.py +0 -0
  89. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.py +0 -0
  90. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.pyi +0 -0
  91. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2_grpc.py +0 -0
  92. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.py +0 -0
  93. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.pyi +0 -0
  94. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2_grpc.py +0 -0
  95. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.py +0 -0
  96. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.pyi +0 -0
  97. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2_grpc.py +0 -0
  98. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.py +0 -0
  99. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.pyi +0 -0
  100. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2_grpc.py +0 -0
  101. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.py +0 -0
  102. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.pyi +0 -0
  103. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2_grpc.py +0 -0
  104. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.py +0 -0
  105. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.pyi +0 -0
  106. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2_grpc.py +0 -0
  107. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.py +0 -0
  108. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.pyi +0 -0
  109. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2_grpc.py +0 -0
  110. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.py +0 -0
  111. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.pyi +0 -0
  112. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2_grpc.py +0 -0
  113. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.py +0 -0
  114. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.pyi +0 -0
  115. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2_grpc.py +0 -0
  116. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/__init__.py +0 -0
  117. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/__init__.py +0 -0
  118. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/__init__.py +0 -0
  119. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/client.py +0 -0
  120. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/enums.py +0 -0
  121. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/logger.py +0 -0
  122. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/__init__.py +0 -0
  123. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/client.py +0 -0
  124. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/models.py +0 -0
  125. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/sync_client.py +0 -0
  126. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/__init__.py +0 -0
  127. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/client.py +0 -0
  128. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/models.py +0 -0
  129. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/sync_client.py +0 -0
  130. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/retry.py +0 -0
  131. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/schema.json +0 -0
  132. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/machines/sync_client.py +0 -0
  133. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/__init__.py +0 -0
  134. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/client.py +0 -0
  135. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/enums.py +0 -0
  136. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/logger.py +0 -0
  137. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/retry.py +0 -0
  138. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/schema.json +0 -0
  139. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/sync_client.py +0 -0
  140. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/__init__.py +0 -0
  141. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/client.py +0 -0
  142. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/models.py +0 -0
  143. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/sync_client.py +0 -0
  144. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/__init__.py +0 -0
  145. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/client.py +0 -0
  146. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/models.py +0 -0
  147. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/sync_client.py +0 -0
  148. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/__init__.py +0 -0
  149. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/client.py +0 -0
  150. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/enums.py +0 -0
  151. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/logger.py +0 -0
  152. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/retry.py +0 -0
  153. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/schema.json +0 -0
  154. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/sync_client.py +0 -0
  155. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/__init__.py +0 -0
  156. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/client.py +0 -0
  157. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/models.py +0 -0
  158. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/sync_client.py +0 -0
  159. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/client.py +0 -0
  160. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/config.py +0 -0
  161. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/discovery.py +0 -0
  162. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/exceptions.py +0 -0
  163. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/__init__.py +0 -0
  164. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/cleaner.py +0 -0
  165. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/helpers/formatting.py +0 -0
  166. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/logging.py +0 -0
  167. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/__init__.py +0 -0
  168. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/agent.py +0 -0
  169. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/base.py +0 -0
  170. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/config.py +0 -0
  171. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/extract.py +0 -0
  172. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/files.py +0 -0
  173. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/models/terminal.py +0 -0
  174. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/py.typed +0 -0
  175. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/__init__.py +0 -0
  176. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/agent.py +0 -0
  177. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/base.py +0 -0
  178. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/__init__.py +0 -0
  179. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/__init__.py +0 -0
  180. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/aio/service.py +0 -0
  181. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/__init__.py +0 -0
  182. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/base/service.py +0 -0
  183. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/core.py +0 -0
  184. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/js/fetch.py +0 -0
  185. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/models.py +0 -0
  186. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/parsing.py +0 -0
  187. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/__init__.py +0 -0
  188. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/browser/sync/service.py +0 -0
  189. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/extract.py +0 -0
  190. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/files.py +0 -0
  191. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/services/terminal.py +0 -0
  192. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/__init__.py +0 -0
  193. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/base.py +0 -0
  194. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/handlers.py +0 -0
  195. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/streaming/terminal.py +0 -0
  196. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/__init__.py +0 -0
  197. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/auth.py +0 -0
  198. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/base.py +0 -0
  199. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/discovery.py +0 -0
  200. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/local.py +0 -0
  201. {cmdop-0.1.17 → cmdop-0.1.18}/src/cmdop/transport/remote.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cmdop
3
- Version: 0.1.17
3
+ Version: 0.1.18
4
4
  Summary: Python SDK for CMDOP agent interaction
5
5
  Project-URL: Homepage, https://cmdop.com
6
6
  Project-URL: Documentation, https://cmdop.com
@@ -148,56 +148,63 @@ health: Health = result.output # Typed!
148
148
  ```python
149
149
  with client.browser.create_session() as b:
150
150
  b.navigate("https://shop.com/products")
151
+ b.close_modal() # Close popups
151
152
 
152
- # DOM extraction
153
- products = b.extract_data(".product-card", '{"name": "h2", "price": ".price"}', limit=100)
153
+ # BeautifulSoup parsing
154
+ soup = b.soup() # SoupWrapper with chainable API
155
+ for item in soup.select(".product"):
156
+ title = item.select_one("h2").text()
157
+ price = item.attr("data-price")
154
158
 
155
- # Get HTML for BeautifulSoup parsing
156
- html = b.get_html("[role='feed']")
157
- soup = b.parse_html(html) # Returns BeautifulSoup object
158
-
159
- # Scroll & extract pattern
159
+ # Human-like scrolling with random delays
160
160
  for _ in range(10):
161
- html = b.get_html(".listings")
162
- # ... parse with soup ...
163
- b.scroll("down", 800)
164
- b.wait_seconds(1.0)
161
+ soup = b.soup(".listings")
162
+ # ... parse ...
163
+ b.scroll("down", 700, human_like=True) # Natural micro-scrolls
164
+ b.wait_random(0.8, 1.5) # Random delay
165
+
166
+ # Scroll inside container (Facebook, Twitter feeds)
167
+ b.scroll("down", 800, container="[role='feed']")
168
+
169
+ # Click all "See more" buttons
170
+ b.click_all_by_text("See more")
165
171
 
166
172
  # JS fetch (bypass CORS, inherit cookies)
167
173
  data = b.fetch_json("https://api.site.com/v1/items")
168
-
169
- # Parallel fetch
170
- results = b.fetch_all({
171
- "users": "https://api.site.com/v1/users",
172
- "orders": "https://api.site.com/v1/orders",
173
- }, credentials=True)
174
174
  ```
175
175
 
176
176
  | Method | Description |
177
177
  |--------|-------------|
178
178
  | `navigate(url)` | Go to URL |
179
179
  | `click(selector)` | Click element |
180
+ | `click_all_by_text(text, role)` | Click all matching elements |
180
181
  | `type(selector, text)` | Type text |
181
182
  | `wait_for(selector, ms)` | Wait for element |
182
183
  | `wait_seconds(n)` | Sleep |
184
+ | `wait_random(min, max)` | Random sleep |
183
185
  | `extract(selector, attr)` | Get text/attr |
184
186
  | `get_html(selector)` | Get HTML |
185
- | `get_text(selector)` | Get text |
187
+ | `soup(selector)` | SoupWrapper |
186
188
  | `parse_html(html)` | → BeautifulSoup |
187
- | `extract_data(item, fields, limit)` | Bulk extract |
188
189
  | `fetch_json(url)` | JS fetch → dict |
189
- | `fetch_all(urls, credentials)` | Parallel fetch |
190
+ | `fetch_all(urls)` | Parallel fetch |
190
191
  | `execute_js(code)` | Run async JS |
191
192
  | `screenshot()` | PNG bytes |
192
- | `scroll(dir, amount)` | Scroll page |
193
+ | `scroll(dir, amount, ...)` | Scroll page/container |
193
194
  | `scroll_to(selector)` | Scroll to element |
194
195
  | `get_scroll_info()` | Position + page size |
195
- | `infinite_scroll(fn, limit)` | Smart scroll loop |
196
196
  | `hover(selector)` | Hover |
197
197
  | `select(selector, value)` | Dropdown select |
198
198
  | `close_modal()` | Close dialogs |
199
199
  | `get/set_cookies()` | Cookie management |
200
200
 
201
+ **scroll() parameters:**
202
+ - `direction`: "up", "down", "left", "right"
203
+ - `amount`: pixels to scroll
204
+ - `smooth`: animate scroll (default True)
205
+ - `human_like`: random micro-scrolls + variation
206
+ - `container`: CSS selector for scroll container
207
+
201
208
  ## SDKBaseModel
202
209
 
203
210
  Auto-cleaning Pydantic model for scraped data:
@@ -107,56 +107,63 @@ health: Health = result.output # Typed!
107
107
  ```python
108
108
  with client.browser.create_session() as b:
109
109
  b.navigate("https://shop.com/products")
110
+ b.close_modal() # Close popups
110
111
 
111
- # DOM extraction
112
- products = b.extract_data(".product-card", '{"name": "h2", "price": ".price"}', limit=100)
112
+ # BeautifulSoup parsing
113
+ soup = b.soup() # SoupWrapper with chainable API
114
+ for item in soup.select(".product"):
115
+ title = item.select_one("h2").text()
116
+ price = item.attr("data-price")
113
117
 
114
- # Get HTML for BeautifulSoup parsing
115
- html = b.get_html("[role='feed']")
116
- soup = b.parse_html(html) # Returns BeautifulSoup object
117
-
118
- # Scroll & extract pattern
118
+ # Human-like scrolling with random delays
119
119
  for _ in range(10):
120
- html = b.get_html(".listings")
121
- # ... parse with soup ...
122
- b.scroll("down", 800)
123
- b.wait_seconds(1.0)
120
+ soup = b.soup(".listings")
121
+ # ... parse ...
122
+ b.scroll("down", 700, human_like=True) # Natural micro-scrolls
123
+ b.wait_random(0.8, 1.5) # Random delay
124
+
125
+ # Scroll inside container (Facebook, Twitter feeds)
126
+ b.scroll("down", 800, container="[role='feed']")
127
+
128
+ # Click all "See more" buttons
129
+ b.click_all_by_text("See more")
124
130
 
125
131
  # JS fetch (bypass CORS, inherit cookies)
126
132
  data = b.fetch_json("https://api.site.com/v1/items")
127
-
128
- # Parallel fetch
129
- results = b.fetch_all({
130
- "users": "https://api.site.com/v1/users",
131
- "orders": "https://api.site.com/v1/orders",
132
- }, credentials=True)
133
133
  ```
134
134
 
135
135
  | Method | Description |
136
136
  |--------|-------------|
137
137
  | `navigate(url)` | Go to URL |
138
138
  | `click(selector)` | Click element |
139
+ | `click_all_by_text(text, role)` | Click all matching elements |
139
140
  | `type(selector, text)` | Type text |
140
141
  | `wait_for(selector, ms)` | Wait for element |
141
142
  | `wait_seconds(n)` | Sleep |
143
+ | `wait_random(min, max)` | Random sleep |
142
144
  | `extract(selector, attr)` | Get text/attr |
143
145
  | `get_html(selector)` | Get HTML |
144
- | `get_text(selector)` | Get text |
146
+ | `soup(selector)` | SoupWrapper |
145
147
  | `parse_html(html)` | → BeautifulSoup |
146
- | `extract_data(item, fields, limit)` | Bulk extract |
147
148
  | `fetch_json(url)` | JS fetch → dict |
148
- | `fetch_all(urls, credentials)` | Parallel fetch |
149
+ | `fetch_all(urls)` | Parallel fetch |
149
150
  | `execute_js(code)` | Run async JS |
150
151
  | `screenshot()` | PNG bytes |
151
- | `scroll(dir, amount)` | Scroll page |
152
+ | `scroll(dir, amount, ...)` | Scroll page/container |
152
153
  | `scroll_to(selector)` | Scroll to element |
153
154
  | `get_scroll_info()` | Position + page size |
154
- | `infinite_scroll(fn, limit)` | Smart scroll loop |
155
155
  | `hover(selector)` | Hover |
156
156
  | `select(selector, value)` | Dropdown select |
157
157
  | `close_modal()` | Close dialogs |
158
158
  | `get/set_cookies()` | Cookie management |
159
159
 
160
+ **scroll() parameters:**
161
+ - `direction`: "up", "down", "left", "right"
162
+ - `amount`: pixels to scroll
163
+ - `smooth`: animate scroll (default True)
164
+ - `human_like`: random micro-scrolls + variation
165
+ - `container`: CSS selector for scroll container
166
+
160
167
  ## SDKBaseModel
161
168
 
162
169
  Auto-cleaning Pydantic model for scraped data:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cmdop"
3
- version = "0.1.17"
3
+ version = "0.1.18"
4
4
  description = "Python SDK for CMDOP agent interaction"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -124,7 +124,7 @@ from cmdop.logging import (
124
124
  get_log_dir,
125
125
  )
126
126
 
127
- __version__ = "0.1.17"
127
+ __version__ = "0.1.18"
128
128
 
129
129
  __all__ = [
130
130
  # Version
@@ -4,9 +4,17 @@ from __future__ import annotations
4
4
 
5
5
  from typing import TYPE_CHECKING, Any
6
6
 
7
+ import asyncio
8
+
7
9
  from cmdop.services.browser.base.session import BaseSession
8
- from cmdop.services.browser.models import BrowserCookie, BrowserState
10
+ from cmdop.services.browser.models import (
11
+ BrowserCookie,
12
+ BrowserState,
13
+ ScrollInfo,
14
+ ScrollResult,
15
+ )
9
16
  from cmdop.services.browser.js import parse_json_result
17
+ from cmdop.services.browser.parsing import parse_html as _parse_html, SoupWrapper
10
18
 
11
19
  if TYPE_CHECKING:
12
20
  from cmdop.services.browser.aio.service import AsyncBrowserService
@@ -158,6 +166,112 @@ class AsyncBrowserSession(BaseSession):
158
166
  self._session_id, item, fields_json, limit
159
167
  )
160
168
 
169
+ # === HTML Parsing ===
170
+
171
+ async def parse_html(self, html: str | None = None, selector: str | None = None) -> "BeautifulSoup":
172
+ """Parse HTML with BeautifulSoup."""
173
+ if html is None:
174
+ html = await self.get_html(selector)
175
+ return _parse_html(html)
176
+
177
+ async def soup(self, selector: str | None = None) -> SoupWrapper:
178
+ """Get page HTML as SoupWrapper."""
179
+ html = await self.get_html(selector)
180
+ return SoupWrapper(html=html)
181
+
182
+ # === Scroll & Navigation ===
183
+
184
+ async def scroll(
185
+ self,
186
+ direction: str = "down",
187
+ amount: int = 500,
188
+ selector: str | None = None,
189
+ smooth: bool = True,
190
+ human_like: bool = False,
191
+ container: str | None = None,
192
+ ) -> ScrollResult:
193
+ """Scroll the page or container."""
194
+ js = self._build_scroll(direction, amount, selector, smooth, human_like, container)
195
+ result = await self.execute_script(js)
196
+ data = parse_json_result(result) or {}
197
+ return ScrollResult(
198
+ success=data.get("success", False),
199
+ scroll_y=int(data.get("scrollY", 0)),
200
+ scrolled_by=int(data.get("scrolledBy", 0)),
201
+ at_bottom=data.get("atBottom", False),
202
+ error=data.get("error"),
203
+ )
204
+
205
+ async def scroll_to(self, selector: str) -> ScrollResult:
206
+ """Scroll element into view."""
207
+ return await self.scroll(selector=selector)
208
+
209
+ async def scroll_to_bottom(self) -> ScrollResult:
210
+ """Scroll to page bottom."""
211
+ js = self._build_scroll_to_bottom()
212
+ result = await self.execute_script(js)
213
+ data = parse_json_result(result) or {}
214
+ return ScrollResult(
215
+ success=data.get("success", False),
216
+ scroll_y=int(data.get("scrollY", 0)),
217
+ scrolled_by=int(data.get("scrolledBy", 0)),
218
+ at_bottom=True,
219
+ )
220
+
221
+ async def get_scroll_info(self) -> ScrollInfo:
222
+ """Get current scroll position and page dimensions."""
223
+ js = self._build_get_scroll_info()
224
+ result = await self.execute_script(js)
225
+ data = parse_json_result(result) or {}
226
+ return ScrollInfo(
227
+ scroll_x=int(data.get("scrollX", 0)),
228
+ scroll_y=int(data.get("scrollY", 0)),
229
+ page_height=int(data.get("pageHeight", 0)),
230
+ page_width=int(data.get("pageWidth", 0)),
231
+ viewport_height=int(data.get("viewportHeight", 0)),
232
+ viewport_width=int(data.get("viewportWidth", 0)),
233
+ at_bottom=data.get("atBottom", False),
234
+ at_top=data.get("atTop", True),
235
+ )
236
+
237
+ # === UI Interaction Helpers ===
238
+
239
+ async def hover(self, selector: str) -> bool:
240
+ """Hover over element."""
241
+ js = self._build_hover(selector)
242
+ result = await self.execute_script(js)
243
+ data = parse_json_result(result) or {}
244
+ return data.get("success", False)
245
+
246
+ async def select(self, selector: str, value: str | None = None, text: str | None = None) -> dict:
247
+ """Select option from dropdown."""
248
+ js = self._build_select(selector, value, text)
249
+ result = await self.execute_script(js)
250
+ return parse_json_result(result) or {}
251
+
252
+ async def close_modal(self, selectors: list[str] | None = None) -> bool:
253
+ """Try to close modal/dialog."""
254
+ js = self._build_close_modal(selectors)
255
+ result = await self.execute_script(js)
256
+ data = parse_json_result(result) or {}
257
+ return data.get("success", False)
258
+
259
+ async def wait_seconds(self, seconds: float) -> None:
260
+ """Wait for specified seconds."""
261
+ await asyncio.sleep(seconds)
262
+
263
+ async def wait_random(self, min_sec: float = 0.5, max_sec: float = 1.5) -> None:
264
+ """Wait for random time between min and max seconds."""
265
+ import random
266
+ await asyncio.sleep(min_sec + random.random() * (max_sec - min_sec))
267
+
268
+ async def click_all_by_text(self, text: str, role: str = "button") -> int:
269
+ """Click all elements containing specific text."""
270
+ js = self._build_click_all_by_text(text, role)
271
+ result = await self.execute_script(js)
272
+ data = parse_json_result(result) or {}
273
+ return data.get("clicked", 0)
274
+
161
275
  # === Context Manager ===
162
276
 
163
277
  async def close(self) -> None:
@@ -10,6 +10,13 @@ from cmdop.services.browser.js import (
10
10
  build_async_js,
11
11
  build_fetch_js,
12
12
  build_fetch_all_js,
13
+ build_scroll_js,
14
+ build_scroll_to_bottom_js,
15
+ build_get_scroll_info_js,
16
+ build_hover_js,
17
+ build_select_js,
18
+ build_close_modal_js,
19
+ build_click_all_by_text_js,
13
20
  parse_json_result,
14
21
  )
15
22
 
@@ -70,3 +77,43 @@ class BaseSession(ABC):
70
77
  def _parse_fetch_all(self, result: Any) -> dict[str, Any]:
71
78
  """Ensure fetch_all returns dict."""
72
79
  return result if isinstance(result, dict) else {}
80
+
81
+ # === Scroll helpers ===
82
+
83
+ def _build_scroll(
84
+ self,
85
+ direction: str,
86
+ amount: int,
87
+ selector: str | None,
88
+ smooth: bool,
89
+ human_like: bool,
90
+ container: str | None = None,
91
+ ) -> str:
92
+ """Build scroll JS."""
93
+ return build_scroll_js(direction, amount, selector, smooth, human_like, container)
94
+
95
+ def _build_scroll_to_bottom(self) -> str:
96
+ """Build scroll to bottom JS."""
97
+ return build_scroll_to_bottom_js()
98
+
99
+ def _build_get_scroll_info(self) -> str:
100
+ """Build get scroll info JS."""
101
+ return build_get_scroll_info_js()
102
+
103
+ # === Interaction helpers ===
104
+
105
+ def _build_hover(self, selector: str) -> str:
106
+ """Build hover JS."""
107
+ return build_hover_js(selector)
108
+
109
+ def _build_select(self, selector: str, value: str | None, text: str | None) -> str:
110
+ """Build select JS."""
111
+ return build_select_js(selector, value, text)
112
+
113
+ def _build_close_modal(self, selectors: list[str] | None) -> str:
114
+ """Build close modal JS."""
115
+ return build_close_modal_js(selectors)
116
+
117
+ def _build_click_all_by_text(self, text: str, role: str) -> str:
118
+ """Build click all by text JS."""
119
+ return build_click_all_by_text_js(text, role)
@@ -28,6 +28,7 @@ from cmdop.services.browser.js.interaction import (
28
28
  build_hover_js,
29
29
  build_select_js,
30
30
  build_close_modal_js,
31
+ build_click_all_by_text_js,
31
32
  )
32
33
 
33
34
  __all__ = [
@@ -46,4 +47,5 @@ __all__ = [
46
47
  "build_hover_js",
47
48
  "build_select_js",
48
49
  "build_close_modal_js",
50
+ "build_click_all_by_text_js",
49
51
  ]
@@ -107,3 +107,31 @@ def build_close_modal_js(
107
107
  return JSON.stringify({{ success: false, error: 'No modal close button found' }});
108
108
  }})()
109
109
  """
110
+
111
+
112
+ def build_click_all_by_text_js(text: str, role: str = "button") -> str:
113
+ """
114
+ Build JS to click all elements containing specific text.
115
+
116
+ Args:
117
+ text: Text to match (case-insensitive)
118
+ role: Element role to filter (default: "button")
119
+
120
+ Returns:
121
+ JS code that returns { clicked: number }
122
+ """
123
+ return f"""
124
+ (function() {{
125
+ const elements = document.querySelectorAll('[role="{role}"]');
126
+ let clicked = 0;
127
+ const targetText = "{text}".toLowerCase();
128
+ elements.forEach(el => {{
129
+ const elText = el.textContent.trim().toLowerCase();
130
+ if (elText === targetText || elText.includes(targetText)) {{
131
+ el.click();
132
+ clicked++;
133
+ }}
134
+ }});
135
+ return JSON.stringify({{ clicked: clicked }});
136
+ }})()
137
+ """
@@ -0,0 +1,224 @@
1
+ """Scroll JavaScript builders for browser automation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+
7
+
8
+ def build_scroll_js(
9
+ direction: str = "down",
10
+ amount: int = 500,
11
+ selector: str | None = None,
12
+ smooth: bool = True,
13
+ human_like: bool = False,
14
+ container: str | None = None,
15
+ ) -> str:
16
+ """
17
+ Build JS for scrolling.
18
+
19
+ Args:
20
+ direction: "up", "down", "left", "right"
21
+ amount: Pixels to scroll (ignored if selector provided)
22
+ selector: CSS selector to scroll element into view
23
+ smooth: Use smooth scroll animation (default True)
24
+ human_like: Add random variations for natural scrolling
25
+ container: CSS selector for scroll container (default: window)
26
+ """
27
+ if selector:
28
+ behavior = "smooth" if smooth else "instant"
29
+ return f"""
30
+ (async function() {{
31
+ const el = document.querySelector("{selector}");
32
+ if (el) {{
33
+ el.scrollIntoView({{ behavior: '{behavior}', block: 'center' }});
34
+ await new Promise(r => setTimeout(r, {300 if smooth else 50}));
35
+ return JSON.stringify({{ success: true, scrollY: window.scrollY }});
36
+ }}
37
+ return JSON.stringify({{ success: false, error: 'Element not found' }});
38
+ }})()
39
+ """
40
+
41
+ behavior = "smooth" if smooth else "instant"
42
+
43
+ # Container scroll target
44
+ container_js = f'document.querySelector("{container}")' if container else "null"
45
+
46
+ if human_like:
47
+ # Human-like scroll with random variation and micro-scrolls
48
+ return f"""
49
+ (async function() {{
50
+ const container = {container_js};
51
+ const scrollTarget = container || window;
52
+ const getScrollY = () => container ? container.scrollTop : window.scrollY;
53
+ const getMaxScroll = () => container
54
+ ? container.scrollHeight - container.clientHeight
55
+ : document.body.scrollHeight - window.innerHeight;
56
+
57
+ const before = getScrollY();
58
+ const baseAmount = {amount};
59
+ const direction = "{direction}";
60
+
61
+ // Random variation ±15%
62
+ const variation = 0.85 + Math.random() * 0.3;
63
+ const actualAmount = Math.round(baseAmount * variation);
64
+
65
+ // Split into 2-4 micro-scrolls
66
+ const steps = 2 + Math.floor(Math.random() * 3);
67
+ const stepAmount = actualAmount / steps;
68
+
69
+ for (let i = 0; i < steps; i++) {{
70
+ const stepVariation = 0.8 + Math.random() * 0.4;
71
+ const thisStep = Math.round(stepAmount * stepVariation);
72
+
73
+ let x = 0, y = 0;
74
+ if (direction === "down") y = thisStep;
75
+ else if (direction === "up") y = -thisStep;
76
+ else if (direction === "right") x = thisStep;
77
+ else if (direction === "left") x = -thisStep;
78
+
79
+ if (container) {{
80
+ container.scrollBy({{ left: x, top: y, behavior: '{behavior}' }});
81
+ }} else {{
82
+ window.scrollBy({{ left: x, top: y, behavior: '{behavior}' }});
83
+ }}
84
+
85
+ // Random micro-pause 50-150ms between steps
86
+ if (i < steps - 1) {{
87
+ await new Promise(r => setTimeout(r, 50 + Math.random() * 100));
88
+ }}
89
+ }}
90
+
91
+ // Wait for smooth scroll to finish
92
+ await new Promise(r => setTimeout(r, {250 if smooth else 50}));
93
+
94
+ const after = getScrollY();
95
+ return JSON.stringify({{
96
+ success: true,
97
+ scrollY: after,
98
+ scrolledBy: after - before,
99
+ atBottom: after >= getMaxScroll() - 50
100
+ }});
101
+ }})()
102
+ """
103
+ else:
104
+ # Standard scroll
105
+ scroll_x = 0
106
+ scroll_y = 0
107
+ if direction == "down":
108
+ scroll_y = amount
109
+ elif direction == "up":
110
+ scroll_y = -amount
111
+ elif direction == "right":
112
+ scroll_x = amount
113
+ elif direction == "left":
114
+ scroll_x = -amount
115
+
116
+ return f"""
117
+ (async function() {{
118
+ const container = {container_js};
119
+ const getScrollY = () => container ? container.scrollTop : window.scrollY;
120
+ const getMaxScroll = () => container
121
+ ? container.scrollHeight - container.clientHeight
122
+ : document.body.scrollHeight - window.innerHeight;
123
+
124
+ const before = getScrollY();
125
+
126
+ if (container) {{
127
+ container.scrollBy({{ left: {scroll_x}, top: {scroll_y}, behavior: '{behavior}' }});
128
+ }} else {{
129
+ window.scrollBy({{ left: {scroll_x}, top: {scroll_y}, behavior: '{behavior}' }});
130
+ }}
131
+
132
+ // Wait for scroll to complete
133
+ await new Promise(r => setTimeout(r, {200 if smooth else 30}));
134
+
135
+ const after = getScrollY();
136
+ return JSON.stringify({{
137
+ success: true,
138
+ scrollY: after,
139
+ scrolledBy: after - before,
140
+ atBottom: after >= getMaxScroll() - 50
141
+ }});
142
+ }})()
143
+ """
144
+
145
+
146
+ def build_scroll_to_bottom_js() -> str:
147
+ """Build JS to scroll to page bottom."""
148
+ return """
149
+ (function() {
150
+ const before = window.scrollY;
151
+ window.scrollTo(0, document.body.scrollHeight);
152
+ return JSON.stringify({
153
+ success: true,
154
+ scrollY: window.scrollY,
155
+ scrolledBy: window.scrollY - before,
156
+ atBottom: true
157
+ });
158
+ })()
159
+ """
160
+
161
+
162
+ def build_infinite_scroll_js(
163
+ seen_keys: list[str],
164
+ key_selector: str = "a[href]",
165
+ key_attr: str = "href",
166
+ container_selector: str = "body",
167
+ ) -> str:
168
+ """
169
+ Build JS for infinite scroll with deduplication.
170
+
171
+ Args:
172
+ seen_keys: List of already seen keys (e.g., URLs)
173
+ key_selector: CSS selector for key elements
174
+ key_attr: Attribute to use as key (default: href)
175
+ container_selector: Container to find elements in
176
+
177
+ Returns:
178
+ JS that returns {new_keys: [...], at_bottom: bool}
179
+ """
180
+ seen_json = json.dumps(seen_keys)
181
+ return f"""
182
+ (function() {{
183
+ const seen = new Set({seen_json});
184
+ const container = document.querySelector("{container_selector}");
185
+ if (!container) return JSON.stringify({{ new_keys: [], at_bottom: true, error: 'Container not found' }});
186
+
187
+ const elements = container.querySelectorAll("{key_selector}");
188
+ const newKeys = [];
189
+
190
+ elements.forEach(el => {{
191
+ const key = el.getAttribute("{key_attr}") || el.textContent.trim();
192
+ if (key && !seen.has(key)) {{
193
+ seen.add(key);
194
+ newKeys.push(key);
195
+ }}
196
+ }});
197
+
198
+ const atBottom = (window.innerHeight + window.scrollY) >= document.body.scrollHeight - 100;
199
+
200
+ return JSON.stringify({{
201
+ new_keys: newKeys,
202
+ at_bottom: atBottom,
203
+ total_seen: seen.size
204
+ }});
205
+ }})()
206
+ """
207
+
208
+
209
+ def build_get_scroll_info_js() -> str:
210
+ """Build JS to get current scroll position and page dimensions."""
211
+ return """
212
+ (function() {
213
+ return JSON.stringify({
214
+ scrollX: window.scrollX,
215
+ scrollY: window.scrollY,
216
+ pageHeight: document.body.scrollHeight,
217
+ pageWidth: document.body.scrollWidth,
218
+ viewportHeight: window.innerHeight,
219
+ viewportWidth: window.innerWidth,
220
+ atBottom: (window.innerHeight + window.scrollY) >= document.body.scrollHeight - 10,
221
+ atTop: window.scrollY <= 10
222
+ });
223
+ })()
224
+ """