cmdop 0.1.16__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 (203) hide show
  1. cmdop-0.1.18/PKG-INFO +256 -0
  2. cmdop-0.1.18/README.md +215 -0
  3. {cmdop-0.1.16 → cmdop-0.1.18}/pyproject.toml +6 -1
  4. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/__init__.py +1 -1
  5. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/session.py +115 -1
  6. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/session.py +47 -0
  7. cmdop-0.1.18/src/cmdop/services/browser/js/__init__.py +51 -0
  8. cmdop-0.1.18/src/cmdop/services/browser/js/core.py +38 -0
  9. cmdop-0.1.16/src/cmdop/services/browser/js.py → cmdop-0.1.18/src/cmdop/services/browser/js/fetch.py +1 -34
  10. cmdop-0.1.18/src/cmdop/services/browser/js/interaction.py +137 -0
  11. cmdop-0.1.18/src/cmdop/services/browser/js/scroll.py +224 -0
  12. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/models.py +32 -0
  13. cmdop-0.1.18/src/cmdop/services/browser/parsing.py +104 -0
  14. cmdop-0.1.18/src/cmdop/services/browser/sync/session.py +467 -0
  15. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/discovery.py +25 -1
  16. cmdop-0.1.16/PKG-INFO +0 -464
  17. cmdop-0.1.16/README.md +0 -426
  18. cmdop-0.1.16/src/cmdop/services/browser/sync/session.py +0 -171
  19. {cmdop-0.1.16 → cmdop-0.1.18}/.gitignore +0 -0
  20. {cmdop-0.1.16 → cmdop-0.1.18}/LICENSE +0 -0
  21. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/__init__.py +0 -0
  22. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.py +0 -0
  23. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2.pyi +0 -0
  24. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/agent_messages_pb2_grpc.py +0 -0
  25. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.py +0 -0
  26. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2.pyi +0 -0
  27. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/common_types_pb2_grpc.py +0 -0
  28. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.py +0 -0
  29. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2.pyi +0 -0
  30. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/control_messages_pb2_grpc.py +0 -0
  31. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/__init__.py +0 -0
  32. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.py +0 -0
  33. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2.pyi +0 -0
  34. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/archive_pb2_grpc.py +0 -0
  35. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.py +0 -0
  36. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2.pyi +0 -0
  37. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/changes_pb2_grpc.py +0 -0
  38. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.py +0 -0
  39. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2.pyi +0 -0
  40. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/common_pb2_grpc.py +0 -0
  41. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.py +0 -0
  42. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2.pyi +0 -0
  43. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/directory_pb2_grpc.py +0 -0
  44. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.py +0 -0
  45. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2.pyi +0 -0
  46. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/file_crud_pb2_grpc.py +0 -0
  47. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.py +0 -0
  48. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2.pyi +0 -0
  49. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/hls_pb2_grpc.py +0 -0
  50. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.py +0 -0
  51. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2.pyi +0 -0
  52. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/requests_pb2_grpc.py +0 -0
  53. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.py +0 -0
  54. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2.pyi +0 -0
  55. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/search_pb2_grpc.py +0 -0
  56. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.py +0 -0
  57. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2.pyi +0 -0
  58. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations/transfer_pb2_grpc.py +0 -0
  59. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.py +0 -0
  60. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2.pyi +0 -0
  61. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_operations_pb2_grpc.py +0 -0
  62. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/__init__.py +0 -0
  63. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.py +0 -0
  64. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2.pyi +0 -0
  65. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/archive_pb2_grpc.py +0 -0
  66. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.py +0 -0
  67. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2.pyi +0 -0
  68. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/directory_pb2_grpc.py +0 -0
  69. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.py +0 -0
  70. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2.pyi +0 -0
  71. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/file_crud_pb2_grpc.py +0 -0
  72. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.py +0 -0
  73. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2.pyi +0 -0
  74. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/hls_pb2_grpc.py +0 -0
  75. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.py +0 -0
  76. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2.pyi +0 -0
  77. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc/search_pb2_grpc.py +0 -0
  78. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.py +0 -0
  79. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2.pyi +0 -0
  80. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/file_rpc_pb2_grpc.py +0 -0
  81. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/message_pool.py +0 -0
  82. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/py.typed +0 -0
  83. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/__init__.py +0 -0
  84. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.py +0 -0
  85. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2.pyi +0 -0
  86. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/agent_pb2_grpc.py +0 -0
  87. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.py +0 -0
  88. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2.pyi +0 -0
  89. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/browser_pb2_grpc.py +0 -0
  90. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.py +0 -0
  91. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2.pyi +0 -0
  92. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/device_pb2_grpc.py +0 -0
  93. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.py +0 -0
  94. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2.pyi +0 -0
  95. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/extract_pb2_grpc.py +0 -0
  96. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.py +0 -0
  97. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2.pyi +0 -0
  98. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/health_pb2_grpc.py +0 -0
  99. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.py +0 -0
  100. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2.pyi +0 -0
  101. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/history_pb2_grpc.py +0 -0
  102. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.py +0 -0
  103. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2.pyi +0 -0
  104. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/lifecycle_pb2_grpc.py +0 -0
  105. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.py +0 -0
  106. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2.pyi +0 -0
  107. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/push_pb2_grpc.py +0 -0
  108. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.py +0 -0
  109. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2.pyi +0 -0
  110. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/session_pb2_grpc.py +0 -0
  111. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.py +0 -0
  112. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2.pyi +0 -0
  113. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages/terminal_pb2_grpc.py +0 -0
  114. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.py +0 -0
  115. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2.pyi +0 -0
  116. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/rpc_messages_pb2_grpc.py +0 -0
  117. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.py +0 -0
  118. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2.pyi +0 -0
  119. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/service_pb2_grpc.py +0 -0
  120. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.py +0 -0
  121. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2.pyi +0 -0
  122. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/_generated/tunnel_pb2_grpc.py +0 -0
  123. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/__init__.py +0 -0
  124. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/__init__.py +0 -0
  125. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/__init__.py +0 -0
  126. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/client.py +0 -0
  127. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/enums.py +0 -0
  128. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/logger.py +0 -0
  129. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/__init__.py +0 -0
  130. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/client.py +0 -0
  131. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/models.py +0 -0
  132. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machine_sharing/sync_client.py +0 -0
  133. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/__init__.py +0 -0
  134. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/client.py +0 -0
  135. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/models.py +0 -0
  136. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/machines__api__machines/sync_client.py +0 -0
  137. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/retry.py +0 -0
  138. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/schema.json +0 -0
  139. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/machines/sync_client.py +0 -0
  140. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/__init__.py +0 -0
  141. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/client.py +0 -0
  142. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/enums.py +0 -0
  143. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/logger.py +0 -0
  144. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/retry.py +0 -0
  145. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/schema.json +0 -0
  146. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/sync_client.py +0 -0
  147. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/__init__.py +0 -0
  148. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/client.py +0 -0
  149. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/models.py +0 -0
  150. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__oauth/sync_client.py +0 -0
  151. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/__init__.py +0 -0
  152. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/client.py +0 -0
  153. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/models.py +0 -0
  154. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/system/system__api__system/sync_client.py +0 -0
  155. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/__init__.py +0 -0
  156. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/client.py +0 -0
  157. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/enums.py +0 -0
  158. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/logger.py +0 -0
  159. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/retry.py +0 -0
  160. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/schema.json +0 -0
  161. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/sync_client.py +0 -0
  162. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/__init__.py +0 -0
  163. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/client.py +0 -0
  164. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/models.py +0 -0
  165. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/api/generated/workspaces/workspaces__api__workspaces/sync_client.py +0 -0
  166. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/client.py +0 -0
  167. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/config.py +0 -0
  168. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/discovery.py +0 -0
  169. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/exceptions.py +0 -0
  170. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/__init__.py +0 -0
  171. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/cleaner.py +0 -0
  172. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/helpers/formatting.py +0 -0
  173. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/logging.py +0 -0
  174. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/__init__.py +0 -0
  175. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/agent.py +0 -0
  176. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/base.py +0 -0
  177. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/config.py +0 -0
  178. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/extract.py +0 -0
  179. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/files.py +0 -0
  180. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/models/terminal.py +0 -0
  181. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/py.typed +0 -0
  182. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/__init__.py +0 -0
  183. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/agent.py +0 -0
  184. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/base.py +0 -0
  185. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/__init__.py +0 -0
  186. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/__init__.py +0 -0
  187. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/aio/service.py +0 -0
  188. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/__init__.py +0 -0
  189. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/base/service.py +0 -0
  190. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/sync/__init__.py +0 -0
  191. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/browser/sync/service.py +0 -0
  192. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/extract.py +0 -0
  193. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/files.py +0 -0
  194. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/services/terminal.py +0 -0
  195. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/__init__.py +0 -0
  196. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/base.py +0 -0
  197. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/handlers.py +0 -0
  198. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/streaming/terminal.py +0 -0
  199. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/__init__.py +0 -0
  200. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/auth.py +0 -0
  201. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/base.py +0 -0
  202. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/local.py +0 -0
  203. {cmdop-0.1.16 → cmdop-0.1.18}/src/cmdop/transport/remote.py +0 -0
cmdop-0.1.18/PKG-INFO ADDED
@@ -0,0 +1,256 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdop
3
+ Version: 0.1.18
4
+ Summary: Python SDK for CMDOP agent interaction
5
+ Project-URL: Homepage, https://cmdop.com
6
+ Project-URL: Documentation, https://cmdop.com
7
+ Project-URL: Repository, https://github.com/markolofsen/cmdop-client
8
+ Author: CMDOP Team
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: agent,automation,cmdop,terminal
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: beautifulsoup4>=4.12.0
23
+ Requires-Dist: grpcio>=1.60.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: lxml>=5.0.0
26
+ Requires-Dist: protobuf>=4.25.0
27
+ Requires-Dist: pydantic-settings>=2.0.0
28
+ Requires-Dist: pydantic>=2.5.0
29
+ Requires-Dist: rich>=13.0.0
30
+ Requires-Dist: toon-python>=0.1.2
31
+ Provides-Extra: dev
32
+ Requires-Dist: beautifulsoup4>=4.12.0; extra == 'dev'
33
+ Requires-Dist: grpcio-tools>=1.60.0; extra == 'dev'
34
+ Requires-Dist: mypy>=1.8.0; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
36
+ Requires-Dist: pytest-cov>=4.1.0; extra == 'dev'
37
+ Requires-Dist: pytest-grpc-aio>=0.3.0; extra == 'dev'
38
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
39
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
40
+ Description-Content-Type: text/markdown
41
+
42
+ # cmdop
43
+
44
+ **Any machine. One API.**
45
+
46
+ ```python
47
+ from cmdop import CMDOPClient
48
+
49
+ with CMDOPClient.remote(api_key="cmd_xxx") as server:
50
+ server.terminal.execute("docker restart app")
51
+ server.files.write("/etc/nginx/nginx.conf", new_config)
52
+ logs = server.files.read("/var/log/app.log")
53
+ ```
54
+
55
+ No SSH. No VPN. No open ports.
56
+
57
+ ---
58
+
59
+ ## How
60
+
61
+ ```
62
+ Your Code ──── Cloud Relay ──── Agent (on server)
63
+
64
+ Outbound only, works through any NAT/firewall
65
+ ```
66
+
67
+ Agent connects out. Your code connects to relay. Done.
68
+
69
+ ---
70
+
71
+ ## Install
72
+
73
+ ```bash
74
+ pip install cmdop
75
+ ```
76
+
77
+ ```python
78
+ from cmdop import CMDOPClient, AsyncCMDOPClient
79
+
80
+ # Remote (via cloud relay)
81
+ with CMDOPClient.remote(api_key="cmd_xxx") as client:
82
+ client.files.list("/home")
83
+
84
+ # Local (direct IPC)
85
+ with CMDOPClient.local() as client:
86
+ client.terminal.execute("ls -la")
87
+
88
+ # Async
89
+ async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
90
+ await client.files.read("/etc/hostname")
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Terminal
96
+
97
+ ```python
98
+ session = server.terminal.create()
99
+ server.terminal.send_input(session.session_id, "kubectl get pods\n")
100
+ output = server.terminal.get_history(session.session_id)
101
+ ```
102
+
103
+ | Method | Description |
104
+ |--------|-------------|
105
+ | `create(shell)` | Start session |
106
+ | `send_input(id, data)` | Send commands |
107
+ | `get_history(id)` | Get output |
108
+ | `resize(id, cols, rows)` | Resize |
109
+ | `send_signal(id, signal)` | SIGINT/SIGTERM |
110
+ | `close(id)` | End session |
111
+
112
+ ## Files
113
+
114
+ ```python
115
+ server.files.list("/var/log")
116
+ server.files.read("/etc/nginx/nginx.conf")
117
+ server.files.write("/tmp/config.json", b'{"key": "value"}')
118
+ ```
119
+
120
+ | Method | Description |
121
+ |--------|-------------|
122
+ | `list(path)` | List dir |
123
+ | `read(path)` | Read file |
124
+ | `write(path, content)` | Write file |
125
+ | `delete(path)` | Delete |
126
+ | `copy/move(src, dst)` | Copy/Move |
127
+ | `mkdir(path)` | Create dir |
128
+ | `info(path)` | Metadata |
129
+
130
+ ## Agent
131
+
132
+ ```python
133
+ from pydantic import BaseModel
134
+
135
+ class Health(BaseModel):
136
+ status: str
137
+ cpu: float
138
+ issues: list[str]
139
+
140
+ result = server.agent.run("Check server health", output_schema=Health)
141
+ health: Health = result.output # Typed!
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Browser
147
+
148
+ ```python
149
+ with client.browser.create_session() as b:
150
+ b.navigate("https://shop.com/products")
151
+ b.close_modal() # Close popups
152
+
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")
158
+
159
+ # Human-like scrolling with random delays
160
+ for _ in range(10):
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")
171
+
172
+ # JS fetch (bypass CORS, inherit cookies)
173
+ data = b.fetch_json("https://api.site.com/v1/items")
174
+ ```
175
+
176
+ | Method | Description |
177
+ |--------|-------------|
178
+ | `navigate(url)` | Go to URL |
179
+ | `click(selector)` | Click element |
180
+ | `click_all_by_text(text, role)` | Click all matching elements |
181
+ | `type(selector, text)` | Type text |
182
+ | `wait_for(selector, ms)` | Wait for element |
183
+ | `wait_seconds(n)` | Sleep |
184
+ | `wait_random(min, max)` | Random sleep |
185
+ | `extract(selector, attr)` | Get text/attr |
186
+ | `get_html(selector)` | Get HTML |
187
+ | `soup(selector)` | → SoupWrapper |
188
+ | `parse_html(html)` | → BeautifulSoup |
189
+ | `fetch_json(url)` | JS fetch → dict |
190
+ | `fetch_all(urls)` | Parallel fetch |
191
+ | `execute_js(code)` | Run async JS |
192
+ | `screenshot()` | PNG bytes |
193
+ | `scroll(dir, amount, ...)` | Scroll page/container |
194
+ | `scroll_to(selector)` | Scroll to element |
195
+ | `get_scroll_info()` | Position + page size |
196
+ | `hover(selector)` | Hover |
197
+ | `select(selector, value)` | Dropdown select |
198
+ | `close_modal()` | Close dialogs |
199
+ | `get/set_cookies()` | Cookie management |
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
+
208
+ ## SDKBaseModel
209
+
210
+ Auto-cleaning Pydantic model for scraped data:
211
+
212
+ ```python
213
+ from cmdop import SDKBaseModel
214
+
215
+ class Product(SDKBaseModel):
216
+ __base_url__ = "https://shop.com"
217
+ name: str = "" # " iPhone 15 \n" → "iPhone 15"
218
+ price: int = 0 # "$1,299.00" → 1299
219
+ rating: float = 0 # "4.5 stars" → 4.5
220
+ url: str = "" # "/p/123" → "https://shop.com/p/123"
221
+
222
+ products = Product.from_list(raw["items"]) # Auto dedupe + filter
223
+ ```
224
+
225
+ ---
226
+
227
+ ## Utilities
228
+
229
+ **Logging:**
230
+ ```python
231
+ from cmdop import get_logger
232
+ log = get_logger(__name__)
233
+ log.info("Starting") # Rich console + auto file logging
234
+ ```
235
+
236
+ **TOON Format (30-50% token savings):**
237
+ ```python
238
+ from cmdop import json_to_toon, JsonCleaner
239
+ toon = json_to_toon({"name": "Alice", "age": 25})
240
+ # → "name: Alice\nage: 25"
241
+ ```
242
+
243
+ ---
244
+
245
+ ## Requirements
246
+
247
+ - Python 3.10+
248
+ - CMDOP agent on target
249
+
250
+ ## Links
251
+
252
+ [cmdop.com](https://cmdop.com)
253
+
254
+ ## License
255
+
256
+ MIT
cmdop-0.1.18/README.md ADDED
@@ -0,0 +1,215 @@
1
+ # cmdop
2
+
3
+ **Any machine. One API.**
4
+
5
+ ```python
6
+ from cmdop import CMDOPClient
7
+
8
+ with CMDOPClient.remote(api_key="cmd_xxx") as server:
9
+ server.terminal.execute("docker restart app")
10
+ server.files.write("/etc/nginx/nginx.conf", new_config)
11
+ logs = server.files.read("/var/log/app.log")
12
+ ```
13
+
14
+ No SSH. No VPN. No open ports.
15
+
16
+ ---
17
+
18
+ ## How
19
+
20
+ ```
21
+ Your Code ──── Cloud Relay ──── Agent (on server)
22
+
23
+ Outbound only, works through any NAT/firewall
24
+ ```
25
+
26
+ Agent connects out. Your code connects to relay. Done.
27
+
28
+ ---
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install cmdop
34
+ ```
35
+
36
+ ```python
37
+ from cmdop import CMDOPClient, AsyncCMDOPClient
38
+
39
+ # Remote (via cloud relay)
40
+ with CMDOPClient.remote(api_key="cmd_xxx") as client:
41
+ client.files.list("/home")
42
+
43
+ # Local (direct IPC)
44
+ with CMDOPClient.local() as client:
45
+ client.terminal.execute("ls -la")
46
+
47
+ # Async
48
+ async with AsyncCMDOPClient.remote(api_key="cmd_xxx") as client:
49
+ await client.files.read("/etc/hostname")
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Terminal
55
+
56
+ ```python
57
+ session = server.terminal.create()
58
+ server.terminal.send_input(session.session_id, "kubectl get pods\n")
59
+ output = server.terminal.get_history(session.session_id)
60
+ ```
61
+
62
+ | Method | Description |
63
+ |--------|-------------|
64
+ | `create(shell)` | Start session |
65
+ | `send_input(id, data)` | Send commands |
66
+ | `get_history(id)` | Get output |
67
+ | `resize(id, cols, rows)` | Resize |
68
+ | `send_signal(id, signal)` | SIGINT/SIGTERM |
69
+ | `close(id)` | End session |
70
+
71
+ ## Files
72
+
73
+ ```python
74
+ server.files.list("/var/log")
75
+ server.files.read("/etc/nginx/nginx.conf")
76
+ server.files.write("/tmp/config.json", b'{"key": "value"}')
77
+ ```
78
+
79
+ | Method | Description |
80
+ |--------|-------------|
81
+ | `list(path)` | List dir |
82
+ | `read(path)` | Read file |
83
+ | `write(path, content)` | Write file |
84
+ | `delete(path)` | Delete |
85
+ | `copy/move(src, dst)` | Copy/Move |
86
+ | `mkdir(path)` | Create dir |
87
+ | `info(path)` | Metadata |
88
+
89
+ ## Agent
90
+
91
+ ```python
92
+ from pydantic import BaseModel
93
+
94
+ class Health(BaseModel):
95
+ status: str
96
+ cpu: float
97
+ issues: list[str]
98
+
99
+ result = server.agent.run("Check server health", output_schema=Health)
100
+ health: Health = result.output # Typed!
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Browser
106
+
107
+ ```python
108
+ with client.browser.create_session() as b:
109
+ b.navigate("https://shop.com/products")
110
+ b.close_modal() # Close popups
111
+
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")
117
+
118
+ # Human-like scrolling with random delays
119
+ for _ in range(10):
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")
130
+
131
+ # JS fetch (bypass CORS, inherit cookies)
132
+ data = b.fetch_json("https://api.site.com/v1/items")
133
+ ```
134
+
135
+ | Method | Description |
136
+ |--------|-------------|
137
+ | `navigate(url)` | Go to URL |
138
+ | `click(selector)` | Click element |
139
+ | `click_all_by_text(text, role)` | Click all matching elements |
140
+ | `type(selector, text)` | Type text |
141
+ | `wait_for(selector, ms)` | Wait for element |
142
+ | `wait_seconds(n)` | Sleep |
143
+ | `wait_random(min, max)` | Random sleep |
144
+ | `extract(selector, attr)` | Get text/attr |
145
+ | `get_html(selector)` | Get HTML |
146
+ | `soup(selector)` | → SoupWrapper |
147
+ | `parse_html(html)` | → BeautifulSoup |
148
+ | `fetch_json(url)` | JS fetch → dict |
149
+ | `fetch_all(urls)` | Parallel fetch |
150
+ | `execute_js(code)` | Run async JS |
151
+ | `screenshot()` | PNG bytes |
152
+ | `scroll(dir, amount, ...)` | Scroll page/container |
153
+ | `scroll_to(selector)` | Scroll to element |
154
+ | `get_scroll_info()` | Position + page size |
155
+ | `hover(selector)` | Hover |
156
+ | `select(selector, value)` | Dropdown select |
157
+ | `close_modal()` | Close dialogs |
158
+ | `get/set_cookies()` | Cookie management |
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
+
167
+ ## SDKBaseModel
168
+
169
+ Auto-cleaning Pydantic model for scraped data:
170
+
171
+ ```python
172
+ from cmdop import SDKBaseModel
173
+
174
+ class Product(SDKBaseModel):
175
+ __base_url__ = "https://shop.com"
176
+ name: str = "" # " iPhone 15 \n" → "iPhone 15"
177
+ price: int = 0 # "$1,299.00" → 1299
178
+ rating: float = 0 # "4.5 stars" → 4.5
179
+ url: str = "" # "/p/123" → "https://shop.com/p/123"
180
+
181
+ products = Product.from_list(raw["items"]) # Auto dedupe + filter
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Utilities
187
+
188
+ **Logging:**
189
+ ```python
190
+ from cmdop import get_logger
191
+ log = get_logger(__name__)
192
+ log.info("Starting") # Rich console + auto file logging
193
+ ```
194
+
195
+ **TOON Format (30-50% token savings):**
196
+ ```python
197
+ from cmdop import json_to_toon, JsonCleaner
198
+ toon = json_to_toon({"name": "Alice", "age": 25})
199
+ # → "name: Alice\nage: 25"
200
+ ```
201
+
202
+ ---
203
+
204
+ ## Requirements
205
+
206
+ - Python 3.10+
207
+ - CMDOP agent on target
208
+
209
+ ## Links
210
+
211
+ [cmdop.com](https://cmdop.com)
212
+
213
+ ## License
214
+
215
+ MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cmdop"
3
- version = "0.1.16"
3
+ version = "0.1.18"
4
4
  description = "Python SDK for CMDOP agent interaction"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -32,6 +32,9 @@ dependencies = [
32
32
  "toon-python>=0.1.2",
33
33
  # Logging
34
34
  "rich>=13.0.0",
35
+ # HTML parsing (browser SDK)
36
+ "beautifulsoup4>=4.12.0",
37
+ "lxml>=5.0.0",
35
38
  ]
36
39
 
37
40
  [project.optional-dependencies]
@@ -44,6 +47,8 @@ dev = [
44
47
  "ruff>=0.1.0",
45
48
  # For proto generation (still uses grpcio-tools with betterproto plugin)
46
49
  "grpcio-tools>=1.60.0",
50
+ # HTML parsing for tests
51
+ "beautifulsoup4>=4.12.0",
47
52
  ]
48
53
 
49
54
  [project.urls]
@@ -124,7 +124,7 @@ from cmdop.logging import (
124
124
  get_log_dir,
125
125
  )
126
126
 
127
- __version__ = "0.1.16"
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)