rf-mcp 0.34.0.dev4__tar.gz → 0.34.0.dev5__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 (263) hide show
  1. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/PKG-INFO +4 -2
  2. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/README.md +1 -1
  3. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/pyproject.toml +10 -1
  4. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/desktop_guidance.py +1 -0
  5. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/keyword_executor.py +91 -23
  6. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/platynui_plugin.py +280 -6
  7. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.gitignore +0 -0
  8. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/@msgpackr-extract/msgpackr-extract-linux-x64/README.md +0 -0
  9. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/@standard-schema/spec/LICENSE +0 -0
  10. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/@standard-schema/spec/README.md +0 -0
  11. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/cross-spawn/LICENSE +0 -0
  12. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/cross-spawn/README.md +0 -0
  13. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/detect-libc/LICENSE +0 -0
  14. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/detect-libc/README.md +0 -0
  15. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/effect/LICENSE +0 -0
  16. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/effect/README.md +0 -0
  17. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/fast-check/LICENSE +0 -0
  18. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/fast-check/README.md +0 -0
  19. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/find-my-way-ts/LICENSE +0 -0
  20. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/find-my-way-ts/README.md +0 -0
  21. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/ini/LICENSE +0 -0
  22. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/ini/README.md +0 -0
  23. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/isexe/LICENSE +0 -0
  24. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/isexe/README.md +0 -0
  25. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/kubernetes-types/README.md +0 -0
  26. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/msgpackr/LICENSE +0 -0
  27. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/msgpackr/README.md +0 -0
  28. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/msgpackr-extract/LICENSE +0 -0
  29. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/msgpackr-extract/README.md +0 -0
  30. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/multipasta/LICENSE +0 -0
  31. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/multipasta/README.md +0 -0
  32. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/node-gyp-build-optional-packages/LICENSE +0 -0
  33. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/node-gyp-build-optional-packages/README.md +0 -0
  34. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/pure-rand/LICENSE +0 -0
  35. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/pure-rand/README.md +0 -0
  36. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/toml/LICENSE +0 -0
  37. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/toml/README.md +0 -0
  38. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/uuid/README.md +0 -0
  39. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/which/LICENSE +0 -0
  40. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/which/README.md +0 -0
  41. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/yaml/LICENSE +0 -0
  42. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/yaml/README.md +0 -0
  43. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/zod/LICENSE +0 -0
  44. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/.kilo/node_modules/zod/README.md +0 -0
  45. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/LICENSE +0 -0
  46. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/__init__.py +0 -0
  47. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/__init__.py +0 -0
  48. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/adapter_factory.py +0 -0
  49. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/browser_adapter.py +0 -0
  50. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/page_state_adapter.py +0 -0
  51. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/playwright_adapter.py +0 -0
  52. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/recovery_adapter.py +0 -0
  53. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/adapters/selenium_adapter.py +0 -0
  54. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/attach/__init__.py +0 -0
  55. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/attach/mcp_attach.py +0 -0
  56. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/compat/__init__.py +0 -0
  57. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/compat/fastmcp_compat.py +0 -0
  58. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/__init__.py +0 -0
  59. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/browser/__init__.py +0 -0
  60. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/browser/browser_library_manager.py +0 -0
  61. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/__init__.py +0 -0
  62. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/desktop_display_safety.py +0 -0
  63. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/desktop_execution_signals.py +0 -0
  64. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/desktop_launch_env.py +0 -0
  65. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/execution_coordinator.py +0 -0
  66. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/external_rf_client.py +0 -0
  67. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/locator_arg_introspection.py +0 -0
  68. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/locator_converter.py +0 -0
  69. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/mobile_capability_service.py +0 -0
  70. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/page_source_service.py +0 -0
  71. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/platynui_focus.py +0 -0
  72. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/rf_native_context_manager.py +0 -0
  73. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/session_manager.py +0 -0
  74. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/suite_execution_service.py +0 -0
  75. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/execution/ui_tree_service.py +0 -0
  76. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/keyword_matcher.py +0 -0
  77. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/library_recommender.py +0 -0
  78. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/nlp_processor.py +0 -0
  79. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/state_manager.py +0 -0
  80. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/test_builder.py +0 -0
  81. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/variables/__init__.py +0 -0
  82. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/components/variables/variable_resolver.py +0 -0
  83. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/config/__init__.py +0 -0
  84. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/config/library_registry.py +0 -0
  85. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/container.py +0 -0
  86. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/__init__.py +0 -0
  87. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/dynamic_keyword_orchestrator.py +0 -0
  88. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/event_bus.py +0 -0
  89. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/keyword_discovery.py +0 -0
  90. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/library_manager.py +0 -0
  91. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/core/session_manager.py +0 -0
  92. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/__init__.py +0 -0
  93. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/__init__.py +0 -0
  94. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/aggregates.py +0 -0
  95. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/entities.py +0 -0
  96. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/events.py +0 -0
  97. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/services.py +0 -0
  98. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/artifact_output/value_objects.py +0 -0
  99. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/__init__.py +0 -0
  100. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/aggregates.py +0 -0
  101. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/entities.py +0 -0
  102. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/events.py +0 -0
  103. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/services.py +0 -0
  104. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/batch_execution/value_objects.py +0 -0
  105. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/__init__.py +0 -0
  106. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/adapters/__init__.py +0 -0
  107. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/adapters/fastmcp_adapter.py +0 -0
  108. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/aggregates.py +0 -0
  109. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/entities.py +0 -0
  110. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/events.py +0 -0
  111. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/repository.py +0 -0
  112. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/security.py +0 -0
  113. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/services.py +0 -0
  114. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/instruction/value_objects.py +0 -0
  115. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/__init__.py +0 -0
  116. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/adapters/__init__.py +0 -0
  117. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/adapters/locator_normalizer_adapter.py +0 -0
  118. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/adapters/mcp_tool.py +0 -0
  119. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/adapters/session_lookup_adapter.py +0 -0
  120. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/aggregates.py +0 -0
  121. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/entities.py +0 -0
  122. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/events.py +0 -0
  123. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/services.py +0 -0
  124. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/intent/value_objects.py +0 -0
  125. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/keyword_resolution/__init__.py +0 -0
  126. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/keyword_resolution/aggregates.py +0 -0
  127. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/keyword_resolution/events.py +0 -0
  128. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/keyword_resolution/services.py +0 -0
  129. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/keyword_resolution/value_objects.py +0 -0
  130. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/__init__.py +0 -0
  131. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/adapters/__init__.py +0 -0
  132. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/adapters/fastmcp_adapter.py +0 -0
  133. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/adapters/sqlite_vec_adapter.py +0 -0
  134. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/aggregates.py +0 -0
  135. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/entities.py +0 -0
  136. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/events.py +0 -0
  137. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/repository.py +0 -0
  138. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/services.py +0 -0
  139. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/memory/value_objects.py +0 -0
  140. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/__init__.py +0 -0
  141. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/aggregates.py +0 -0
  142. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/entities.py +0 -0
  143. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/events.py +0 -0
  144. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/services.py +0 -0
  145. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/recovery/value_objects.py +0 -0
  146. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/__init__.py +0 -0
  147. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/aggregates.py +0 -0
  148. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/events.py +0 -0
  149. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/repository.py +0 -0
  150. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/services.py +0 -0
  151. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/response_optimization/value_objects.py +0 -0
  152. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/shared/__init__.py +0 -0
  153. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/shared/kernel.py +0 -0
  154. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/__init__.py +0 -0
  155. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/aggregates.py +0 -0
  156. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/delta_aggregates.py +0 -0
  157. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/delta_entities.py +0 -0
  158. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/delta_events.py +0 -0
  159. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/delta_service.py +0 -0
  160. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/delta_value_objects.py +0 -0
  161. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/diff_service.py +0 -0
  162. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/entities.py +0 -0
  163. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/events.py +0 -0
  164. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/list_folding.py +0 -0
  165. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/models.py +0 -0
  166. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/repository.py +0 -0
  167. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/services.py +0 -0
  168. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/snapshot/value_objects.py +0 -0
  169. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/__init__.py +0 -0
  170. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/aggregates.py +0 -0
  171. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/entities.py +0 -0
  172. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/events.py +0 -0
  173. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/keyword_classifier.py +0 -0
  174. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/services.py +0 -0
  175. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/timeout/value_objects.py +0 -0
  176. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/__init__.py +0 -0
  177. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/aggregates.py +0 -0
  178. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/entities.py +0 -0
  179. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/events.py +0 -0
  180. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/repository.py +0 -0
  181. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/services.py +0 -0
  182. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/token_accounting/value_objects.py +0 -0
  183. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/__init__.py +0 -0
  184. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/adapters/__init__.py +0 -0
  185. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/adapters/fastmcp_adapter.py +0 -0
  186. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/aggregates.py +0 -0
  187. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/entities.py +0 -0
  188. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/events.py +0 -0
  189. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/repository.py +0 -0
  190. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/services.py +0 -0
  191. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/domains/tool_profile/value_objects.py +0 -0
  192. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/entry.py +0 -0
  193. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/__init__.py +0 -0
  194. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/api.py +0 -0
  195. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/apps.py +0 -0
  196. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/asgi.py +0 -0
  197. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/bridge.py +0 -0
  198. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/config.py +0 -0
  199. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/controller.py +0 -0
  200. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/devserver.py +0 -0
  201. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/django_app.py +0 -0
  202. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/static/frontend/app.js +0 -0
  203. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/static/frontend/base.css +0 -0
  204. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/templates/frontend/index.html +0 -0
  205. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/templates/frontend/layout.html +0 -0
  206. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/urls.py +0 -0
  207. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/frontend/views.py +0 -0
  208. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/__init__.py +0 -0
  209. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/browser_models.py +0 -0
  210. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/config_models.py +0 -0
  211. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/execution_models.py +0 -0
  212. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/library_models.py +0 -0
  213. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/models/session_models.py +0 -0
  214. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/__init__.py +0 -0
  215. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/adapters.py +0 -0
  216. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/cli.py +0 -0
  217. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/codecs.py +0 -0
  218. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/diagnostics.py +0 -0
  219. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/installer.py +0 -0
  220. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/onboarding/manifest.py +0 -0
  221. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/__init__.py +0 -0
  222. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/collector.py +0 -0
  223. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/compression_learner.py +0 -0
  224. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/folding_learner.py +0 -0
  225. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/instruction_hooks.py +0 -0
  226. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/instruction_learner.py +0 -0
  227. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/page_analyzer.py +0 -0
  228. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/pattern_store.py +0 -0
  229. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/ref_learner.py +0 -0
  230. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/optimization/timeout_learner.py +0 -0
  231. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/__init__.py +0 -0
  232. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/base.py +0 -0
  233. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/__init__.py +0 -0
  234. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/browser_plugin.py +0 -0
  235. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/definitions.py +0 -0
  236. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/requests_plugin.py +0 -0
  237. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/builtin/selenium_plugin.py +0 -0
  238. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/contracts.py +0 -0
  239. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/discovery.py +0 -0
  240. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/plugins/manager.py +0 -0
  241. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/server.py +0 -0
  242. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/__init__.py +0 -0
  243. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/argument_processor.py +0 -0
  244. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/enhanced_response_serializer.py +0 -0
  245. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/enhanced_serialization_integration.py +0 -0
  246. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/hints.py +0 -0
  247. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/libdoc_argument_parser.py +0 -0
  248. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/library_checker.py +0 -0
  249. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/library_detection.py +0 -0
  250. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/library_detector.py +0 -0
  251. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/library_loading_validator.py +0 -0
  252. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/logging_setup.py +0 -0
  253. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/requests_guidance.py +0 -0
  254. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/response_serializer.py +0 -0
  255. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/rf_libdoc_integration.py +0 -0
  256. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/rf_native_type_converter.py +0 -0
  257. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/rf_variables_compatibility.py +0 -0
  258. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/sampling.py +0 -0
  259. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/server_integration.py +0 -0
  260. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/session_resolution.py +0 -0
  261. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/token_efficient_output.py +0 -0
  262. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/validation.py +0 -0
  263. {rf_mcp-0.34.0.dev4 → rf_mcp-0.34.0.dev5}/src/robotmcp/utils/visual_guidance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-mcp
3
- Version: 0.34.0.dev4
3
+ Version: 0.34.0.dev5
4
4
  Summary: Robot Framework MCP Server - Natural Language Test Automation Bridge
5
5
  Project-URL: Homepage, https://github.com/manykarim/rf-mcp
6
6
  Project-URL: Repository, https://github.com/manykarim/rf-mcp
@@ -21,9 +21,11 @@ Requires-Dist: tomlkit>=0.12.0
21
21
  Provides-Extra: all
22
22
  Requires-Dist: django<5.0,>=4.2; extra == 'all'
23
23
  Requires-Dist: model2vec>=0.4.0; extra == 'all'
24
+ Requires-Dist: platynui-cli==0.13.0.dev2; (python_version >= '3.12') and extra == 'all'
24
25
  Requires-Dist: robotframework-appiumlibrary; extra == 'all'
25
26
  Requires-Dist: robotframework-browser; extra == 'all'
26
27
  Requires-Dist: robotframework-databaselibrary; extra == 'all'
28
+ Requires-Dist: robotframework-platynui==0.13.0.dev2; (python_version >= '3.12') and extra == 'all'
27
29
  Requires-Dist: robotframework-requests; extra == 'all'
28
30
  Requires-Dist: robotframework-seleniumlibrary; extra == 'all'
29
31
  Requires-Dist: sqlite-vec>=0.1.6; extra == 'all'
@@ -228,7 +230,7 @@ uv pip install --pre rf-mcp[desktop] # PlatynUI new-core (Rust) native desktop
228
230
  uv pip install rf-mcp[frontend] # Django-based web frontend dashboard
229
231
  uv pip install rf-mcp[memory] # Persistent semantic memory (sqlite-vec + model2vec)
230
232
  uv pip install rf-mcp[semantic] # Sentence-transformers for find_keywords embedding ranking
231
- uv pip install rf-mcp[all] # All optional Robot Framework libraries (desktop is separate — prerelease/3.12+)
233
+ uv pip install rf-mcp[all] # All optional Robot Framework libraries (includes desktop on Python 3.12+)
232
234
 
233
235
  # Alternatively, add to an existing uv project
234
236
  uv init
@@ -171,7 +171,7 @@ uv pip install --pre rf-mcp[desktop] # PlatynUI new-core (Rust) native desktop
171
171
  uv pip install rf-mcp[frontend] # Django-based web frontend dashboard
172
172
  uv pip install rf-mcp[memory] # Persistent semantic memory (sqlite-vec + model2vec)
173
173
  uv pip install rf-mcp[semantic] # Sentence-transformers for find_keywords embedding ranking
174
- uv pip install rf-mcp[all] # All optional Robot Framework libraries (desktop is separate — prerelease/3.12+)
174
+ uv pip install rf-mcp[all] # All optional Robot Framework libraries (includes desktop on Python 3.12+)
175
175
 
176
176
  # Alternatively, add to an existing uv project
177
177
  uv init
@@ -4,7 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rf-mcp"
7
- version = "0.34.0.dev4"
7
+ # 0.34.0.dev5: harden-platynui-stuck-key-release — extend the platynui-windows-runtime
8
+ # stuck-key net from modifiers-only to a tracked held-key registry (releases
9
+ # non-modifier keys too) plus persisted-state recovery across hard process kills.
10
+ version = "0.34.0.dev5"
8
11
  description = "Robot Framework MCP Server - Natural Language Test Automation Bridge"
9
12
  authors = [{ name = "Many Kasiriha" }]
10
13
  license = { text = "Apache-2.0" }
@@ -65,6 +68,12 @@ all = [
65
68
  "sqlite-vec>=0.1.6",
66
69
  "model2vec>=0.4.0",
67
70
  "tiktoken>=0.7.0",
71
+ # Desktop automation is part of [all]. Referenced as a self-extra so the
72
+ # PlatynUI pins stay defined in exactly one place (see `desktop` above) —
73
+ # bumping them there is enough. The `python_version >= "3.12"` markers ride
74
+ # along, so on 3.10/3.11 this contributes nothing and [all] still resolves.
75
+ # The exact-pinned prerelease is accepted without a global --pre.
76
+ "rf-mcp[desktop]",
68
77
  ]
69
78
  # NOTE: dev tooling lives in [dependency-groups].dev (PEP 735), which is what
70
79
  # `uv sync` / CI use. The former [project.optional-dependencies].dev extra was
@@ -30,6 +30,7 @@ _LOCATOR_CRIB: List[str] = [
30
30
  "Launch the app first (Process.Start Process), then Query for its control:Frame before acting.",
31
31
  "Read a value back with Get Attribute on the scoped node; GTK text views expose native:Text.CharacterCount (not live text content).",
32
32
  "Take Screenshot signature is (descriptor, filename, rect) — the FIRST positional is a node descriptor, not a filename; pass filename= for a path.",
33
+ "Prefer the atomic 'Keyboard Type' (self-contained press+release) for shortcuts, e.g. Keyboard Type <descriptor> <Ctrl+A>. A bare 'Keyboard Press' sends key-DOWN ONLY and MUST be paired with 'Keyboard Release' — an unpaired Press (esp. a non-modifier like a letter/F-key/Escape) leaves a key physically held on the OS.",
33
34
  "For the full locator cookbook call get_locator_guidance (topic defaults to the desktop/PlatynUI chapter).",
34
35
  ]
35
36
 
@@ -1914,15 +1914,22 @@ class KeywordExecutor:
1914
1914
  # causing it to go to stderr instead of the MCP transport.
1915
1915
  # The lock also prevents concurrent Selenium WebDriver access.
1916
1916
  async with self._execution_lock:
1917
- # F16 (change: fix-platynui-windows-runtime): a desktop keyboard
1918
- # keyword that raises or returns failure may have been killed
1919
- # mid-chord, leaving a modifier physically held at the OS level
1920
- # (the operator's keyboard wedged until reboot). Release all
1921
- # modifiers on failure/exception. A SUCCESSFUL Keyboard Press
1922
- # legitimately holds a key for a later Keyboard Release, so we do
1923
- # NOT release on success. release_all_modifiers() is a no-op when
1924
- # nothing is held.
1917
+ # F16 + harden-platynui-stuck-key-release: a desktop keyboard keyword
1918
+ # that raises or returns failure may have been killed mid-chord,
1919
+ # leaving a key physically held at the OS level (the operator's
1920
+ # keyboard wedged). We track the EXACT keys held (incl. non-modifiers)
1921
+ # so teardown/failure/exit release precisely those. A SUCCESSFUL
1922
+ # Keyboard Press legitimately holds a key for a later Keyboard
1923
+ # Release, so we do NOT release on success — but we DO record it.
1925
1924
  _kbd = self._is_desktop_keyboard_keyword(session, keyword)
1925
+ _kbd_kind, _kbd_seq = (
1926
+ self._desktop_keyboard_op(keyword, arguments) if _kbd else (None, None)
1927
+ )
1928
+ # Record intended-held keys BEFORE dispatch so a kill mid-call is
1929
+ # covered by the persisted state file (Press holds; Type holds then
1930
+ # releases within the same call).
1931
+ if _kbd_kind in ("press", "type") and _kbd_seq:
1932
+ self._record_pressed_keys(_kbd_seq)
1926
1933
  try:
1927
1934
  result = await self._execute_keyword_serialized(
1928
1935
  session, keyword, arguments, browser_library_manager,
@@ -1939,8 +1946,9 @@ class KeywordExecutor:
1939
1946
  # dispatches). keyboard_release is a fast input dispatch, not a
1940
1947
  # tree query, so the brief on-loop cost is acceptable.
1941
1948
  if _kbd:
1942
- self._release_desktop_modifiers()
1949
+ self._release_desktop_keys()
1943
1950
  raise
1951
+ _released_here = False
1944
1952
  if (
1945
1953
  _kbd
1946
1954
  and isinstance(result, dict)
@@ -1952,28 +1960,86 @@ class KeywordExecutor:
1952
1960
  and not self._is_steering_downgrade(result)
1953
1961
  ):
1954
1962
  # F4: offload the release off the event loop on the normal
1955
- # failure-return path (no cancellation concern here).
1956
- await asyncio.to_thread(self._release_desktop_modifiers)
1957
- # F16 steering (wording only): point the agent at the atomic
1958
- # Keyboard Type, which cannot leave a key held.
1963
+ # failure-return path (no cancellation concern here). Releases
1964
+ # the exact tracked held set (incl. non-modifiers) + clears state.
1965
+ await asyncio.to_thread(self._release_desktop_keys)
1966
+ _released_here = True
1967
+ # Steering: point the agent at the atomic Keyboard Type, which
1968
+ # cannot leave a key held.
1959
1969
  try:
1960
1970
  hints = result.setdefault("hints", [])
1961
1971
  if isinstance(hints, list):
1962
1972
  hints.append({
1963
1973
  "type": "platynui_keyboard_release_safety",
1964
1974
  "message": (
1965
- "Released all keyboard modifiers after this "
1966
- "keyboard keyword failed. Prefer the atomic "
1967
- "'Keyboard Type <Ctrl+A>' (self-contained "
1975
+ "Released all held keyboard keys (including any "
1976
+ "non-modifier such as a letter/F-key/Escape) "
1977
+ "after this keyboard keyword failed. Prefer the "
1978
+ "atomic 'Keyboard Type <Ctrl+A>' (self-contained "
1968
1979
  "press+release) over a bare 'Keyboard Press', "
1969
- "which sends key-DOWN only and must be paired "
1980
+ "which sends key-DOWN only and MUST be paired "
1970
1981
  "with 'Keyboard Release'."
1971
1982
  ),
1972
1983
  })
1973
1984
  except Exception: # pragma: no cover - defensive
1974
1985
  pass
1986
+ # On a SUCCESSFUL Keyboard Type / Keyboard Release, the paired key-UP
1987
+ # already happened — clear those keys from the registry. (Press keeps
1988
+ # its recorded keys held until an explicit Keyboard Release.)
1989
+ if not _released_here and _kbd_kind in ("type", "release") and _kbd_seq:
1990
+ self._record_released_keys(_kbd_seq)
1975
1991
  return result
1976
1992
 
1993
+ @staticmethod
1994
+ def _desktop_keyboard_op(keyword, arguments):
1995
+ """Return ``(kind, sequence)`` for a desktop keyboard keyword, where
1996
+ kind is ``"press"``/``"type"``/``"release"`` and sequence is the key
1997
+ text argument (2nd positional, or a ``text=`` named arg). Non-raising;
1998
+ returns ``(None, None)`` when it cannot classify. change:
1999
+ harden-platynui-stuck-key-release."""
2000
+ try:
2001
+ norm = str(keyword or "").lower().rsplit(".", 1)[-1].strip()
2002
+ kind = {
2003
+ "keyboard press": "press",
2004
+ "keyboard type": "type",
2005
+ "keyboard release": "release",
2006
+ }.get(norm)
2007
+ if kind is None:
2008
+ return (None, None)
2009
+ seq = None
2010
+ args = list(arguments or [])
2011
+ for a in args:
2012
+ if isinstance(a, str) and a.startswith("text="):
2013
+ seq = a[len("text="):]
2014
+ break
2015
+ if seq is None and len(args) >= 2:
2016
+ seq = args[1]
2017
+ return (kind, seq)
2018
+ except Exception: # pragma: no cover - defensive
2019
+ return (None, None)
2020
+
2021
+ @staticmethod
2022
+ def _record_pressed_keys(sequence) -> None:
2023
+ """Record keys a desktop Press/Type is about to hold. Never raises.
2024
+ change: harden-platynui-stuck-key-release."""
2025
+ try:
2026
+ from robotmcp.plugins.builtin.platynui_plugin import record_pressed_keys
2027
+
2028
+ record_pressed_keys(sequence)
2029
+ except Exception as exc: # pragma: no cover - defensive
2030
+ logger.debug("record_pressed_keys failed: %s", exc)
2031
+
2032
+ @staticmethod
2033
+ def _record_released_keys(sequence) -> None:
2034
+ """Remove keys released by a desktop Release/Type from the registry.
2035
+ Never raises. change: harden-platynui-stuck-key-release."""
2036
+ try:
2037
+ from robotmcp.plugins.builtin.platynui_plugin import record_released_keys
2038
+
2039
+ record_released_keys(sequence)
2040
+ except Exception as exc: # pragma: no cover - defensive
2041
+ logger.debug("record_released_keys failed: %s", exc)
2042
+
1977
2043
  @staticmethod
1978
2044
  def _is_steering_downgrade(result: dict) -> bool:
1979
2045
  """True when a desktop keyword's success was flipped to failure by the
@@ -1993,17 +2059,19 @@ class KeywordExecutor:
1993
2059
  return bool(result.get("steering_confidence") == SC_CONTRADICTED)
1994
2060
 
1995
2061
  @staticmethod
1996
- def _release_desktop_modifiers() -> None:
1997
- """Best-effort release of all held keyboard modifiers via the PlatynUI
1998
- runtime (change: fix-platynui-windows-runtime, F16). Never raises."""
2062
+ def _release_desktop_keys() -> None:
2063
+ """Best-effort release of the EXACT tracked held-key set (incl.
2064
+ non-modifiers) via the PlatynUI runtime, clearing the registry + state
2065
+ file. Falls back to a modifier blast internally. Never raises.
2066
+ change: harden-platynui-stuck-key-release."""
1999
2067
  try:
2000
2068
  from robotmcp.plugins.builtin.platynui_plugin import (
2001
- release_all_modifiers,
2069
+ release_tracked_keys,
2002
2070
  )
2003
2071
 
2004
- release_all_modifiers()
2072
+ release_tracked_keys()
2005
2073
  except Exception as exc: # pragma: no cover - defensive
2006
- logger.debug("desktop modifier release failed: %s", exc)
2074
+ logger.debug("desktop key release failed: %s", exc)
2007
2075
 
2008
2076
  async def _execute_keyword_serialized(
2009
2077
  self,
@@ -173,6 +173,21 @@ _RELEASE_ALL_SEQUENCE = "<LCtrl+RCtrl+LAlt+RAlt+AltGr+LShift+RShift+LWin+RWin>"
173
173
  # bind (guarded under _RUNTIME_LOCK), so a killed process still releases keys.
174
174
  _RELEASE_HANDLERS_REGISTERED = False
175
175
 
176
+ # --- Held-key registry (change: harden-platynui-stuck-key-release) -----------
177
+ #
178
+ # The modifiers-only F16 blast (_RELEASE_ALL_SEQUENCE) cannot lift a stuck
179
+ # NON-modifier key (a bare ``Keyboard Press A``/``<F12>``/``<Escape>`` that was
180
+ # never released). The registry records the EXACT keys rf-mcp is holding so the
181
+ # teardown/failure/exit paths release precisely those, and it is mirrored to a
182
+ # per-PID state file so a HARD kill (SIGKILL/TerminateProcess — which run
183
+ # neither atexit nor SIGTERM) is recovered by the next desktop session start.
184
+ _HELD_KEYS: set[str] = set()
185
+ _HELD_KEYS_LOCK = threading.RLock()
186
+ # Subdir under the OS temp dir holding ``held_<pid>.json`` files. A live process
187
+ # only ever replays a file owned by a DEAD pid, so a concurrent healthy session
188
+ # is never robbed of a key it is deliberately holding.
189
+ _HELD_KEYS_DIRNAME = "robotmcp_platynui_held_keys"
190
+
176
191
 
177
192
  def runtime_unavailable_reason() -> Optional[str]:
178
193
  """Classify why the PlatynUI runtime is unavailable, or None when it is
@@ -270,6 +285,259 @@ def release_all_modifiers() -> bool:
270
285
  return False
271
286
 
272
287
 
288
+ # --- Held-key registry: normalize / record / release / recover --------------
289
+ #
290
+ # change: harden-platynui-stuck-key-release.
291
+
292
+
293
+ def _normalize_key_tokens(sequence: Optional[str]) -> list[str]:
294
+ """Parse a PlatynUI key sequence into the individual key tokens that a
295
+ ``Keyboard Press`` of that sequence leaves HELD (key-DOWN, no key-UP).
296
+
297
+ Best-effort and never raises. ``<Ctrl+A>`` -> ``["Ctrl", "A"]``; ``<F12>``
298
+ -> ``["F12"]``; a bare ``A`` -> ``["A"]``; literal text ``hi`` ->
299
+ ``["h", "i"]``. Bracketed groups split on ``+``; unbracketed characters
300
+ each become their own token. Whitespace and empty tokens are dropped.
301
+ """
302
+ tokens: list[str] = []
303
+ if not sequence:
304
+ return tokens
305
+ try:
306
+ i = 0
307
+ n = len(sequence)
308
+ while i < n:
309
+ ch = sequence[i]
310
+ if ch == "<":
311
+ end = sequence.find(">", i + 1)
312
+ if end == -1:
313
+ # Unterminated '<' — treat the rest as literal chars.
314
+ for c in sequence[i + 1:]:
315
+ if not c.isspace():
316
+ tokens.append(c)
317
+ break
318
+ inner = sequence[i + 1:end]
319
+ for part in inner.split("+"):
320
+ part = part.strip()
321
+ if part:
322
+ tokens.append(part)
323
+ i = end + 1
324
+ else:
325
+ if not ch.isspace():
326
+ tokens.append(ch)
327
+ i += 1
328
+ except Exception as exc: # pragma: no cover - defensive
329
+ logger.debug("_normalize_key_tokens failed for %r: %s", sequence, exc)
330
+ return tokens
331
+
332
+
333
+ def _release_sequence_for(tokens) -> str:
334
+ """Build a PlatynUI release sequence that lifts each token individually,
335
+ e.g. ``["A", "Ctrl"]`` -> ``"<A><Ctrl>"``."""
336
+ return "".join(f"<{t}>" for t in tokens)
337
+
338
+
339
+ def record_pressed_keys(sequence: Optional[str]) -> None:
340
+ """Record the keys a ``Keyboard Press``/``Keyboard Type`` is about to hold,
341
+ and mirror the held set to the per-PID state file so a hard kill is
342
+ recoverable. Never raises. change: harden-platynui-stuck-key-release."""
343
+ tokens = _normalize_key_tokens(sequence)
344
+ if not tokens:
345
+ return
346
+ with _HELD_KEYS_LOCK:
347
+ _HELD_KEYS.update(tokens)
348
+ _write_state_locked()
349
+
350
+
351
+ def record_released_keys(sequence: Optional[str]) -> None:
352
+ """Remove keys released by ``Keyboard Release`` (or the paired UP inside
353
+ ``Keyboard Type``) from the registry, updating the state file. Never
354
+ raises. change: harden-platynui-stuck-key-release."""
355
+ tokens = _normalize_key_tokens(sequence)
356
+ if not tokens:
357
+ return
358
+ with _HELD_KEYS_LOCK:
359
+ for t in tokens:
360
+ _HELD_KEYS.discard(t)
361
+ _write_state_locked()
362
+
363
+
364
+ def release_tracked_keys() -> bool:
365
+ """Release EXACTLY the keys the registry records as held (including
366
+ non-modifiers), then defensively blast all modifiers, and clear the
367
+ registry + state file.
368
+
369
+ Best-effort and non-raising; a no-op observationally when nothing is held
370
+ (releasing a not-held key has no effect). Never starts the runtime solely
371
+ to release — acts only when the runtime is already open (mirrors
372
+ ``release_all_modifiers``). change: harden-platynui-stuck-key-release.
373
+ """
374
+ if _RUNTIME is None or _RUNTIME_STATE != "open":
375
+ return False
376
+ with _HELD_KEYS_LOCK:
377
+ held = sorted(_HELD_KEYS)
378
+ dispatched = False
379
+ try:
380
+ if held:
381
+ _RUNTIME.keyboard_release(_release_sequence_for(held))
382
+ dispatched = True
383
+ # Fallback blast: covers any modifier that slipped registry tracking.
384
+ _RUNTIME.keyboard_release(_RELEASE_ALL_SEQUENCE)
385
+ dispatched = True
386
+ except Exception as exc: # pragma: no cover - env dependent
387
+ logger.debug("release_tracked_keys dispatch failed: %s", exc)
388
+ _HELD_KEYS.clear()
389
+ _clear_state_locked()
390
+ return dispatched
391
+
392
+
393
+ # --- Per-PID state file (hard-kill recovery) --------------------------------
394
+
395
+
396
+ def _held_keys_dir() -> Optional[str]:
397
+ """Return (creating if needed) the directory holding per-PID held-key state
398
+ files, or None on failure. Never raises."""
399
+ try:
400
+ import tempfile
401
+
402
+ d = os.path.join(tempfile.gettempdir(), _HELD_KEYS_DIRNAME)
403
+ os.makedirs(d, exist_ok=True)
404
+ return d
405
+ except Exception as exc: # pragma: no cover - defensive
406
+ logger.debug("_held_keys_dir failed: %s", exc)
407
+ return None
408
+
409
+
410
+ def _state_file_path(pid: Optional[int] = None) -> Optional[str]:
411
+ d = _held_keys_dir()
412
+ if d is None:
413
+ return None
414
+ return os.path.join(d, f"held_{pid if pid is not None else os.getpid()}.json")
415
+
416
+
417
+ def _write_state_locked() -> None:
418
+ """Atomically write the current held set (with owning PID) to this process's
419
+ state file, or delete the file when the set is empty. Caller holds
420
+ ``_HELD_KEYS_LOCK``. Never raises."""
421
+ path = _state_file_path()
422
+ if path is None:
423
+ return
424
+ try:
425
+ if not _HELD_KEYS:
426
+ _clear_state_locked()
427
+ return
428
+ import json
429
+
430
+ payload = json.dumps({"pid": os.getpid(), "keys": sorted(_HELD_KEYS)})
431
+ tmp = f"{path}.{os.getpid()}.tmp"
432
+ with open(tmp, "w", encoding="utf-8") as fh:
433
+ fh.write(payload)
434
+ os.replace(tmp, path)
435
+ except Exception as exc: # pragma: no cover - defensive
436
+ logger.debug("_write_state_locked failed: %s", exc)
437
+
438
+
439
+ def _clear_state_locked() -> None:
440
+ """Delete this process's state file. Caller holds ``_HELD_KEYS_LOCK``.
441
+ Never raises."""
442
+ path = _state_file_path()
443
+ if path is None:
444
+ return
445
+ try:
446
+ if os.path.exists(path):
447
+ os.remove(path)
448
+ except Exception as exc: # pragma: no cover - defensive
449
+ logger.debug("_clear_state_locked failed: %s", exc)
450
+
451
+
452
+ def _pid_alive(pid: int) -> bool:
453
+ """Best-effort liveness check for ``pid``. On any uncertainty returns True
454
+ (fail-safe: do not replay a file that might belong to a live process)."""
455
+ if pid <= 0:
456
+ return False
457
+ if pid == os.getpid():
458
+ return True
459
+ try:
460
+ if sys.platform == "win32":
461
+ import ctypes
462
+
463
+ PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
464
+ STILL_ACTIVE = 259
465
+ kernel32 = ctypes.windll.kernel32
466
+ handle = kernel32.OpenProcess(
467
+ PROCESS_QUERY_LIMITED_INFORMATION, False, pid
468
+ )
469
+ if not handle:
470
+ return False # cannot open -> gone (or access denied ~ rare)
471
+ try:
472
+ code = ctypes.c_ulong()
473
+ if kernel32.GetExitCodeProcess(handle, ctypes.byref(code)):
474
+ return code.value == STILL_ACTIVE
475
+ return True
476
+ finally:
477
+ kernel32.CloseHandle(handle)
478
+ else:
479
+ try:
480
+ os.kill(pid, 0)
481
+ return True
482
+ except ProcessLookupError:
483
+ return False
484
+ except PermissionError:
485
+ return True # exists but not ours
486
+ except Exception as exc: # pragma: no cover - defensive
487
+ logger.debug("_pid_alive(%s) failed: %s", pid, exc)
488
+ return True
489
+
490
+
491
+ def recover_orphaned_held_keys() -> int:
492
+ """Replay key-UPs for any state file owned by a DEAD pid (a prior run
493
+ hard-killed with keys held), then delete the file. Files owned by a live
494
+ pid are left untouched. Returns the number of files recovered. Best-effort;
495
+ never raises; never starts the runtime. change:
496
+ harden-platynui-stuck-key-release."""
497
+ if _RUNTIME is None or _RUNTIME_STATE != "open":
498
+ return 0
499
+ d = _held_keys_dir()
500
+ if d is None:
501
+ return 0
502
+ recovered = 0
503
+ try:
504
+ import json
505
+
506
+ for name in os.listdir(d):
507
+ if not (name.startswith("held_") and name.endswith(".json")):
508
+ continue
509
+ path = os.path.join(d, name)
510
+ try:
511
+ with open(path, "r", encoding="utf-8") as fh:
512
+ data = json.loads(fh.read() or "{}")
513
+ except Exception:
514
+ # Unreadable/partial file: remove it defensively.
515
+ try:
516
+ os.remove(path)
517
+ except Exception:
518
+ pass
519
+ continue
520
+ pid = int(data.get("pid", -1) or -1)
521
+ keys = data.get("keys") or []
522
+ if pid == os.getpid() or _pid_alive(pid):
523
+ continue # ours or a live session's — do not touch
524
+ try:
525
+ if keys:
526
+ _RUNTIME.keyboard_release(_release_sequence_for(keys))
527
+ # Defensive modifier blast for the orphaned session too.
528
+ _RUNTIME.keyboard_release(_RELEASE_ALL_SEQUENCE)
529
+ recovered += 1
530
+ except Exception as exc: # pragma: no cover - env dependent
531
+ logger.debug("orphan replay dispatch failed: %s", exc)
532
+ try:
533
+ os.remove(path)
534
+ except Exception:
535
+ pass
536
+ except Exception as exc: # pragma: no cover - defensive
537
+ logger.debug("recover_orphaned_held_keys failed: %s", exc)
538
+ return recovered
539
+
540
+
273
541
  def _register_release_handlers_once() -> None:
274
542
  """Arm atexit + SIGTERM handlers that release held modifiers on process
275
543
  exit (F16). Called once from ``get_runtime`` under ``_RUNTIME_LOCK`` after
@@ -283,7 +551,7 @@ def _register_release_handlers_once() -> None:
283
551
  try:
284
552
  import atexit
285
553
 
286
- atexit.register(release_all_modifiers)
554
+ atexit.register(release_tracked_keys)
287
555
  except Exception as exc: # pragma: no cover - defensive
288
556
  logger.debug("atexit release registration failed: %s", exc)
289
557
  try:
@@ -293,7 +561,7 @@ def _register_release_handlers_once() -> None:
293
561
  _prior = signal.getsignal(signal.SIGTERM)
294
562
 
295
563
  def _release_on_sigterm(signum, frame): # pragma: no cover - signal path
296
- release_all_modifiers()
564
+ release_tracked_keys()
297
565
  if callable(_prior) and _prior not in (
298
566
  signal.SIG_DFL,
299
567
  signal.SIG_IGN,
@@ -644,6 +912,11 @@ class PlatynUILibraryPlugin(StaticLibraryPlugin):
644
912
  # process-kill safety net. Registration is idempotent and the
645
913
  # release itself no-ops until the runtime is open. F16.
646
914
  _register_release_handlers_once()
915
+ # Recover keys left HELD by a prior process that was hard-killed
916
+ # (SIGKILL/TerminateProcess ran no atexit/SIGTERM handler): replay
917
+ # key-UPs from any state file owned by a dead pid, then clear it.
918
+ # change: harden-platynui-stuck-key-release.
919
+ recover_orphaned_held_keys()
647
920
  # Defensively clear any modifier a PRIOR crashed run left held
648
921
  # (no-op unless the runtime is already open) — F16.
649
922
  release_all_modifiers()
@@ -651,14 +924,15 @@ class PlatynUILibraryPlugin(StaticLibraryPlugin):
651
924
  logger.debug("PlatynUI session-start hook failed: %s", exc)
652
925
 
653
926
  def on_session_end(self, session: "ExecutionSession") -> None:
654
- """Release any held keyboard modifiers when a PlatynUI desktop session
655
- closes (change: fix-platynui-windows-runtime, F16).
927
+ """Release any held keyboard keys when a PlatynUI desktop session closes
928
+ (change: fix-platynui-windows-runtime, F16; harden-platynui-stuck-key-release).
656
929
 
657
930
  ``session_manager.on_session_end`` invokes this for every plugin; the
658
931
  release is a no-op when the runtime is not open or nothing is held, so
659
- it is safe for non-desktop sessions too. Never raises."""
932
+ it is safe for non-desktop sessions too. Never raises. Releases the
933
+ exact tracked held-key set (incl. non-modifiers), not just modifiers."""
660
934
  try:
661
- release_all_modifiers()
935
+ release_tracked_keys()
662
936
  except Exception as exc: # pragma: no cover - defensive
663
937
  logger.debug("PlatynUI session-end release failed: %s", exc)
664
938
 
File without changes