clerk-sdk 0.4.17__tar.gz → 0.5.0.dev0__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 (87) hide show
  1. clerk_sdk-0.5.0.dev0/.github/workflows/ci.yaml +39 -0
  2. clerk_sdk-0.5.0.dev0/.github/workflows/pypi_publish.yml +33 -0
  3. clerk_sdk-0.5.0.dev0/.gitignore +86 -0
  4. {clerk_sdk-0.4.17/clerk_sdk.egg-info → clerk_sdk-0.5.0.dev0}/PKG-INFO +13 -26
  5. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/__init__.py +1 -1
  6. clerk_sdk-0.5.0.dev0/clerk/development/cli.py +113 -0
  7. clerk_sdk-0.5.0.dev0/clerk/development/gui/test_session.py +327 -0
  8. clerk_sdk-0.5.0.dev0/clerk/development/init_project.py +325 -0
  9. clerk_sdk-0.5.0.dev0/clerk/development/schema/fetch_schema.py +339 -0
  10. clerk_sdk-0.5.0.dev0/clerk/development/templates/exceptions.py.template +16 -0
  11. clerk_sdk-0.5.0.dev0/clerk/development/templates/main_basic.py.template +22 -0
  12. clerk_sdk-0.5.0.dev0/clerk/development/templates/main_gui.py.template +52 -0
  13. clerk_sdk-0.5.0.dev0/clerk/development/templates/rollbacks.py.template +17 -0
  14. clerk_sdk-0.5.0.dev0/clerk/development/templates/states.py.template +15 -0
  15. clerk_sdk-0.5.0.dev0/clerk/development/templates/transitions.py.template +26 -0
  16. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_actions/base.py +17 -1
  17. clerk_sdk-0.5.0.dev0/clerk/gui_automation/ui_state_machine/Readme.md +79 -0
  18. clerk_sdk-0.5.0.dev0/clerk/utils/__init__.py +0 -0
  19. clerk_sdk-0.5.0.dev0/pyproject.toml +44 -0
  20. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/requirements.txt +2 -0
  21. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/setup.py +3 -3
  22. clerk_sdk-0.5.0.dev0/tests/conftest.py +6 -0
  23. clerk_sdk-0.5.0.dev0/uv.lock +408 -0
  24. clerk_sdk-0.4.17/PKG-INFO +0 -254
  25. clerk_sdk-0.4.17/clerk_sdk.egg-info/SOURCES.txt +0 -67
  26. clerk_sdk-0.4.17/clerk_sdk.egg-info/dependency_links.txt +0 -1
  27. clerk_sdk-0.4.17/clerk_sdk.egg-info/requires.txt +0 -14
  28. clerk_sdk-0.4.17/clerk_sdk.egg-info/top_level.txt +0 -1
  29. clerk_sdk-0.4.17/pyproject.toml +0 -3
  30. clerk_sdk-0.4.17/setup.cfg +0 -4
  31. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/LICENSE +0 -0
  32. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/MANIFEST.in +0 -0
  33. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/README.md +0 -0
  34. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/base.py +0 -0
  35. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/client.py +0 -0
  36. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/decorator/__init__.py +0 -0
  37. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/decorator/models.py +0 -0
  38. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/decorator/task_decorator.py +0 -0
  39. {clerk_sdk-0.4.17/clerk/exceptions → clerk_sdk-0.5.0.dev0/clerk/development}/__init__.py +0 -0
  40. {clerk_sdk-0.4.17/clerk/gui_automation → clerk_sdk-0.5.0.dev0/clerk/exceptions}/__init__.py +0 -0
  41. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/exceptions/exceptions.py +0 -0
  42. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/exceptions/remote_device.py +0 -0
  43. {clerk_sdk-0.4.17/clerk/gui_automation/action_model → clerk_sdk-0.5.0.dev0/clerk/gui_automation}/__init__.py +0 -0
  44. {clerk_sdk-0.4.17/clerk/gui_automation/exceptions → clerk_sdk-0.5.0.dev0/clerk/gui_automation/action_model}/__init__.py +0 -0
  45. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/action_model/model.py +0 -0
  46. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/action_model/utils.py +0 -0
  47. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/client.py +0 -0
  48. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/client_actor/__init__.py +0 -0
  49. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/client_actor/client_actor.py +0 -0
  50. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/client_actor/exception.py +0 -0
  51. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/client_actor/model.py +0 -0
  52. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/decorators/__init__.py +0 -0
  53. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/decorators/gui_automation.py +0 -0
  54. {clerk_sdk-0.4.17/clerk/gui_automation/exceptions/modality → clerk_sdk-0.5.0.dev0/clerk/gui_automation/exceptions}/__init__.py +0 -0
  55. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/exceptions/agent_manager.py +0 -0
  56. {clerk_sdk-0.4.17/clerk/gui_automation/ui_state_inspector → clerk_sdk-0.5.0.dev0/clerk/gui_automation/exceptions/modality}/__init__.py +0 -0
  57. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/exceptions/modality/exc.py +0 -0
  58. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/exceptions/websocket.py +0 -0
  59. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/requirements.txt +0 -0
  60. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_actions/__init__.py +0 -0
  61. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_actions/actions.py +0 -0
  62. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_actions/support.py +0 -0
  63. {clerk_sdk-0.4.17/clerk/models → clerk_sdk-0.5.0.dev0/clerk/gui_automation/ui_state_inspector}/__init__.py +0 -0
  64. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_inspector/gui_vision.py +0 -0
  65. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_inspector/models.py +0 -0
  66. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_machine/__init__.py +0 -0
  67. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_machine/ai_recovery.py +0 -0
  68. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_machine/decorators.py +0 -0
  69. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_machine/exceptions.py +0 -0
  70. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/gui_automation/ui_state_machine/state_machine.py +0 -0
  71. {clerk_sdk-0.4.17/clerk/utils → clerk_sdk-0.5.0.dev0/clerk/models}/__init__.py +0 -0
  72. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/document.py +0 -0
  73. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/document_statuses.py +0 -0
  74. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/file.py +0 -0
  75. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/remote_device.py +0 -0
  76. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/response_model.py +0 -0
  77. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/models/ui_operator.py +0 -0
  78. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/utils/logger.py +0 -0
  79. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/clerk/utils/save_artifact.py +0 -0
  80. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_base.py +0 -0
  81. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_client.py +0 -0
  82. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_document_models.py +0 -0
  83. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_exceptions.py +0 -0
  84. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_file_models.py +0 -0
  85. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_gui_automation.py +0 -0
  86. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_task_decorator.py +0 -0
  87. {clerk_sdk-0.4.17 → clerk_sdk-0.5.0.dev0}/tests/test_utils.py +0 -0
@@ -0,0 +1,39 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [master]
6
+
7
+ jobs:
8
+ test:
9
+ name: Test (Python ${{ matrix.python-version }})
10
+ runs-on: ubuntu-latest
11
+
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.11", "3.12"]
15
+
16
+ env:
17
+ __TEST: "true"
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Python ${{ matrix.python-version }}
24
+ uses: actions/setup-python@v5
25
+ with:
26
+ python-version: ${{ matrix.python-version }}
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ python -m pip install --upgrade pip
31
+ pip install -r requirements.txt
32
+ pip install -r ./clerk/gui_automation/requirements.txt
33
+ pip install build twine pytest
34
+
35
+ - name: Run tests
36
+ run: pytest
37
+
38
+ - name: Build package
39
+ run: python -m build
@@ -0,0 +1,33 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build-and-publish:
9
+ name: Build and Publish to PyPI
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout code
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.10"
20
+
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install build twine
25
+
26
+ - name: Build package
27
+ run: python -m build
28
+
29
+ - name: Publish to PyPI
30
+ env:
31
+ TWINE_USERNAME: __token__
32
+ TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
33
+ run: twine upload dist/*
@@ -0,0 +1,86 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Virtual environments
7
+ .venv/
8
+ venv/
9
+ env/
10
+ ENV/
11
+ .venv.bak/
12
+ venv.bak/
13
+
14
+ # PyInstaller
15
+ *.manifest
16
+ *.spec
17
+
18
+ # Installer logs
19
+ pip-log.txt
20
+ pip-delete-this-directory.txt
21
+
22
+ # Unit test / coverage reports
23
+ htmlcov/
24
+ .tox/
25
+ .nox/
26
+ .coverage
27
+ .coverage.*
28
+ .cache
29
+ nosetests.xml
30
+ coverage.xml
31
+ *.cover
32
+ .hypothesis/
33
+ .pytest_cache/
34
+
35
+ # Build artifacts
36
+ build/
37
+ dist/
38
+ *.egg-info/
39
+ .eggs/
40
+ *.egg
41
+
42
+ # PyPI upload tools
43
+ *.log
44
+ *.bak
45
+
46
+ # Editor-specific files
47
+ .vscode/
48
+ .idea/
49
+ *.swp
50
+ *~
51
+
52
+ # macOS
53
+ .DS_Store
54
+
55
+ # Windows
56
+ Thumbs.db
57
+ ehthumbs.db
58
+ Desktop.ini
59
+
60
+ # Jupyter
61
+ .ipynb_checkpoints/
62
+
63
+ # mypy
64
+ .mypy_cache/
65
+ .dmypy.json
66
+ dmypy.json
67
+
68
+ # profiling
69
+ .prof
70
+
71
+ # dotenv
72
+ .env
73
+ .env.*
74
+
75
+ # Wheel & tarball artifacts
76
+ *.whl
77
+ *.tar.gz
78
+ *.zip
79
+
80
+ # Python project metadata
81
+ MANIFEST
82
+
83
+ # Coverage
84
+ .pytest_cache/
85
+
86
+ data/*
@@ -1,39 +1,26 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clerk-sdk
3
- Version: 0.4.17
3
+ Version: 0.5.0.dev0
4
4
  Summary: Library for interacting with Clerk
5
- Home-page: https://github.com/F-ONE-Group/clerk_pypi
6
- Author: F-ONE Group
7
- Author-email: admin@f-one.group
8
- Classifier: Programming Language :: Python :: 3
5
+ Project-URL: Homepage, https://github.com/F-ONE-Group/clerk_pypi
6
+ Author-email: F-One <contact@f-one.group>
7
+ License-File: LICENSE
9
8
  Classifier: License :: OSI Approved :: MIT License
10
9
  Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
11
  Requires-Python: >=3.11
12
- Description-Content-Type: text/markdown
13
- License-File: LICENSE
14
- Requires-Dist: pydantic<3.0.0,>=2.0.0
15
12
  Requires-Dist: backoff<3.0.0,>=2.0.0
13
+ Requires-Dist: pydantic<3.0.0,>=2.0.0
14
+ Requires-Dist: python-dotenv>=1.0.0
16
15
  Requires-Dist: requests<3.0.0,>=2.32.3
16
+ Requires-Dist: rich>=14.2.0
17
17
  Provides-Extra: all
18
- Requires-Dist: pydantic<3.0.0,>=2.0.0; extra == "all"
19
- Requires-Dist: backoff<3.0.0,>=2.0.0; extra == "all"
20
- Requires-Dist: requests<3.0.0,>=2.32.3; extra == "all"
21
- Requires-Dist: networkx<4.0.0,>=3.5.0; extra == "all"
22
- Requires-Dist: websockets>=15.0.1; extra == "all"
18
+ Requires-Dist: networkx<4.0.0,>=3.5.0; extra == 'all'
19
+ Requires-Dist: websockets>=15.0.1; extra == 'all'
23
20
  Provides-Extra: gui-automation
24
- Requires-Dist: networkx<4.0.0,>=3.5.0; extra == "gui-automation"
25
- Requires-Dist: websockets>=15.0.1; extra == "gui-automation"
26
- Dynamic: author
27
- Dynamic: author-email
28
- Dynamic: classifier
29
- Dynamic: description
30
- Dynamic: description-content-type
31
- Dynamic: home-page
32
- Dynamic: license-file
33
- Dynamic: provides-extra
34
- Dynamic: requires-dist
35
- Dynamic: requires-python
36
- Dynamic: summary
21
+ Requires-Dist: networkx<4.0.0,>=3.5.0; extra == 'gui-automation'
22
+ Requires-Dist: websockets>=15.0.1; extra == 'gui-automation'
23
+ Description-Content-Type: text/markdown
37
24
 
38
25
  # Clerk Python SDK
39
26
 
@@ -1,4 +1,4 @@
1
1
  from .client import Clerk
2
2
 
3
3
 
4
- __version__ = "0.4.17"
4
+ __version__ = "0.5.0.dev0"
@@ -0,0 +1,113 @@
1
+ """Clerk CLI - Unified command-line interface for Clerk development tools"""
2
+ import os
3
+ import sys
4
+ import argparse
5
+ from pathlib import Path
6
+ from dotenv import load_dotenv
7
+
8
+
9
+ def find_project_root() -> Path:
10
+ """Find the project root by looking for common markers"""
11
+ cwd = Path.cwd()
12
+
13
+ project_root_files = ["pyproject.toml", ".env"]
14
+
15
+ # Check current directory and parents
16
+ for path in [cwd] + list(cwd.parents):
17
+ for marker in project_root_files:
18
+ if (path / marker).exists():
19
+ return path
20
+
21
+ return cwd
22
+
23
+
24
+ def main():
25
+ """Main CLI entry point with subcommands"""
26
+ # Find project root and load environment variables from there
27
+ project_root = find_project_root()
28
+ dotenv_path = project_root / ".env"
29
+ load_dotenv(dotenv_path)
30
+
31
+ parser = argparse.ArgumentParser(
32
+ prog="clerk",
33
+ description="Clerk development tools",
34
+ epilog="Run 'clerk <command> --help' for more information on a command."
35
+ )
36
+
37
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
38
+
39
+ # Init project subcommand
40
+ init_parser = subparsers.add_parser(
41
+ "init", help="Initialize a new Clerk custom code project"
42
+ )
43
+ init_parser.add_argument(
44
+ "--target-dir",
45
+ type=str,
46
+ default=None,
47
+ help="Target directory for the project (default: ./src)",
48
+ )
49
+
50
+ # GUI command group
51
+ gui_parser = subparsers.add_parser(
52
+ "gui",
53
+ help="GUI automation commands"
54
+ )
55
+ gui_subparsers = gui_parser.add_subparsers(dest="gui_command", help="GUI subcommands")
56
+
57
+ # GUI connect subcommand
58
+ gui_connect_parser = gui_subparsers.add_parser(
59
+ "connect",
60
+ help="Start interactive GUI automation test session"
61
+ )
62
+
63
+ # Schema command group
64
+ schema_parser = subparsers.add_parser(
65
+ "schema",
66
+ help="Schema management commands"
67
+ )
68
+ schema_subparsers = schema_parser.add_subparsers(dest="schema_command", help="Schema subcommands")
69
+
70
+ # Schema fetch subcommand
71
+ schema_fetch_parser = schema_subparsers.add_parser(
72
+ "fetch",
73
+ help="Fetch and generate Pydantic models from project schema"
74
+ )
75
+
76
+ args = parser.parse_args()
77
+
78
+ # Show help if no command specified
79
+ if not args.command:
80
+ parser.print_help()
81
+ sys.exit(1)
82
+
83
+ # Route to appropriate handler
84
+ if args.command == "init":
85
+ from clerk.development.init_project import main_with_args
86
+
87
+ main_with_args(gui_automation=None, target_dir=args.target_dir)
88
+
89
+ elif args.command == "gui":
90
+ if not hasattr(args, 'gui_command') or not args.gui_command:
91
+ gui_parser.print_help()
92
+ sys.exit(1)
93
+
94
+ if args.gui_command == "connect":
95
+ from clerk.development.gui.test_session import main as gui_main
96
+ gui_main()
97
+
98
+ elif args.command == "schema":
99
+ if not hasattr(args, 'schema_command') or not args.schema_command:
100
+ schema_parser.print_help()
101
+ sys.exit(1)
102
+
103
+ if args.schema_command == "fetch":
104
+ from clerk.development.schema.fetch_schema import main_with_args
105
+ project_id = os.getenv("PROJECT_ID")
106
+ if not project_id:
107
+ print("Error: PROJECT_ID environment variable not set.")
108
+ sys.exit(1)
109
+ main_with_args(project_id, project_root)
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()
@@ -0,0 +1,327 @@
1
+ import sys
2
+ from pathlib import Path
3
+ import time
4
+ from datetime import datetime
5
+ import traceback
6
+ import importlib
7
+ from typing import Any
8
+
9
+ from dotenv import load_dotenv
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.text import Text
13
+
14
+ from clerk.gui_automation.ui_actions.actions import (
15
+ File,
16
+ LeftClick,
17
+ RightClick,
18
+ DoubleClick,
19
+ PressKeys,
20
+ SendKeys,
21
+ WaitFor,
22
+ Scroll,
23
+ OpenApplication,
24
+ ForceCloseApplication,
25
+ SaveFiles,
26
+ DeleteFiles,
27
+ GetFile,
28
+ MaximizeWindow,
29
+ MinimizeWindow,
30
+ CloseWindow,
31
+ ActivateWindow,
32
+ GetText,
33
+ PasteText,
34
+ BaseAction
35
+ )
36
+ from clerk.gui_automation.decorators import gui_automation
37
+ from clerk.decorator.models import ClerkCodePayload, Document
38
+ from clerk.gui_automation.ui_state_machine.state_machine import ScreenPilot
39
+ from clerk.gui_automation.ui_state_inspector.gui_vision import Vision, BaseState
40
+
41
+
42
+ # Initialize rich console
43
+ console = Console()
44
+
45
+ # Store session state
46
+ SESSION_FILE = Path(".test_session_active")
47
+ ACTION_HISTORY = []
48
+ VISION_CLIENT = Vision()
49
+
50
+
51
+ def find_project_root() -> Path:
52
+ """Find the project root by looking for common markers"""
53
+ cwd = Path.cwd()
54
+
55
+ project_root_files = ["pyproject.toml"]
56
+
57
+ # Check current directory and parents
58
+ for path in [cwd] + list(cwd.parents):
59
+ for marker in project_root_files:
60
+ if (path / marker).exists():
61
+ return path
62
+
63
+ return cwd
64
+
65
+
66
+ def reload_states() -> int:
67
+ """Reload states from conventional paths. Returns number of states loaded."""
68
+ project_root = find_project_root()
69
+
70
+ # Common module paths where states might be defined
71
+ # These are module paths (dot-separated), not file paths
72
+ state_module_paths = ["src.gui.states", "states"]
73
+
74
+ loaded_count = 0
75
+
76
+ # Add project root to sys.path if not already there
77
+ if str(project_root) not in sys.path:
78
+ sys.path.insert(0, str(project_root))
79
+
80
+ # Try to import/reload each module path
81
+ for module_path in state_module_paths:
82
+ try:
83
+ # Reload if already imported, otherwise import fresh
84
+ if module_path in sys.modules:
85
+ importlib.reload(sys.modules[module_path])
86
+ else:
87
+ importlib.import_module(module_path)
88
+ loaded_count += 1
89
+ except Exception:
90
+ continue
91
+
92
+ return loaded_count
93
+
94
+
95
+ def get_registered_states() -> dict:
96
+ """Get all registered states from ScreenPilot"""
97
+ states = {}
98
+ for state_name, data in ScreenPilot._graph.nodes(data=True):
99
+ state_cls = data.get("cls")
100
+ if state_cls:
101
+ states[state_name] = {
102
+ "description": getattr(state_cls, "description", "No description"),
103
+ "class": state_cls,
104
+ }
105
+ return states
106
+
107
+
108
+ def classify_current_state() -> tuple[bool, str, str]:
109
+ """Classify the current GUI state using Vision. Reloads states first."""
110
+ try:
111
+ # Always reload states to pick up any changes
112
+ with console.status("[dim]Reloading states...", spinner="dots") as status:
113
+ reload_states()
114
+ status.update("[green]+[/green] Reloaded states")
115
+ time.sleep(0.3) # Brief pause to show success
116
+
117
+ states = get_registered_states()
118
+
119
+ if not states:
120
+ return (
121
+ False,
122
+ "",
123
+ "No states found. Make sure state definitions exist in your project.",
124
+ )
125
+
126
+ # Convert to format expected by Vision.classify_state
127
+ possible_states = [
128
+ {"id": name, "description": data["description"]}
129
+ for name, data in states.items()
130
+ ]
131
+
132
+ with console.status(
133
+ "[dim]Classifying current state (waiting for AI)...", spinner="dots"
134
+ ):
135
+ # Pass output_model=None to get tuple instead of BaseModel
136
+ result: BaseState = VISION_CLIENT.classify_state(possible_states) # type: ignore[arg-type]
137
+
138
+ console.print("[green]+[/green] Classification complete")
139
+ return True, result.id, result.description
140
+ except Exception:
141
+ return False, "", f"Classification failed: {traceback.format_exc()}"
142
+
143
+
144
+ def print_welcome():
145
+ """Print welcome message"""
146
+ title = Text("GUI Automation Interactive Test Session", style="bold cyan")
147
+ panel = Panel(title, border_style="cyan", padding=(1, 2))
148
+ console.print()
149
+ console.print(panel)
150
+ console.print()
151
+ console.print("[bold blue]Commands:[/bold blue]")
152
+ console.print(
153
+ " [dim]classify_state: Classify current GUI state (auto-reloads states)[/dim]"
154
+ )
155
+ console.print(" [dim]exit: End session[/dim]")
156
+ console.print()
157
+ console.print("[bold blue]Testing actions:[/bold blue]")
158
+ console.print(" [dim]Type an action and press Enter to execute[/dim]")
159
+ console.print()
160
+
161
+
162
+ def perform_single_action(action_string: str) -> tuple[bool, Any, str]:
163
+ """Execute a single action and return success status, result, and error message"""
164
+ try:
165
+ # Ensure action has .do() call
166
+ if not "do(" in action_string:
167
+ action_string = f"{action_string}.do()"
168
+
169
+ # Execute and capture result
170
+ result = eval(action_string)
171
+ return True, result, ""
172
+ except Exception as e:
173
+ error_msg = traceback.format_exc()
174
+ return False, None, error_msg
175
+
176
+
177
+ def handle_special_command(command: str) -> tuple[bool, str]:
178
+ """Handle special commands like classify. Returns (is_special, message)"""
179
+ command = command.strip()
180
+
181
+ # Classify command
182
+ if command == "classify_state":
183
+ success, state_id, description = classify_current_state()
184
+ if success:
185
+ # Return empty string since console.print handles it
186
+ console.print()
187
+ console.print(f"[green]Current State:[/green] [bold]{state_id}[/bold]")
188
+ console.print(f" [dim]{description}[/dim]")
189
+ console.print()
190
+ return True, ""
191
+ else:
192
+ console.print(f"[red]{description}[/red]")
193
+ return True, ""
194
+
195
+ return False, ""
196
+
197
+
198
+ def format_result(result):
199
+ """Format action result for rich display"""
200
+ if result is None:
201
+ return "[dim](no return value)[/dim]"
202
+ elif isinstance(result, bool):
203
+ color = "green" if result else "yellow"
204
+ return f"[{color}]{result}[/{color}]"
205
+ elif isinstance(result, (str, int, float)):
206
+ return f"[cyan]{repr(result)}[/cyan]"
207
+ else:
208
+ return f"[cyan]{type(result).__name__}: {str(result)[:100]}[/cyan]"
209
+
210
+
211
+ @gui_automation()
212
+ def start_interactive_session(payload: ClerkCodePayload):
213
+ """Start an interactive test session with websocket connection"""
214
+ session_start = datetime.now()
215
+ action_count = 0
216
+
217
+ print_welcome()
218
+
219
+ # The gui_automation decorator establishes the connection before this function runs
220
+ # By the time we get here, connection is already established
221
+ console.print("[green]+[/green] WebSocket connection established")
222
+ console.print()
223
+
224
+ # Mark session as active
225
+ SESSION_FILE.touch()
226
+
227
+ try:
228
+ while True:
229
+ # Get input from user
230
+ try:
231
+ action_string = console.input(
232
+ "[bold blue]command/action>[/bold blue] "
233
+ ).strip()
234
+ except EOFError:
235
+ break
236
+
237
+ # Check for exit command
238
+ if action_string.lower() in ["exit", "quit", "q"]:
239
+ break
240
+
241
+ # Skip empty input
242
+ if not action_string:
243
+ continue
244
+
245
+ # Check if it's a special command
246
+ is_special, message = handle_special_command(action_string)
247
+ if is_special:
248
+ if message: # Only print if there's a message
249
+ console.print(message)
250
+ continue
251
+
252
+ # Record action
253
+ ACTION_HISTORY.append(
254
+ {"timestamp": datetime.now(), "action": action_string, "success": None}
255
+ )
256
+
257
+ # Execute action with status
258
+ start_time = time.time()
259
+
260
+ with console.status(
261
+ "[dim]Executing action (waiting for tool)...", spinner="dots"
262
+ ):
263
+ success, result, error_msg = perform_single_action(action_string)
264
+ execution_time = time.time() - start_time
265
+
266
+ # Show completion message
267
+ if success:
268
+ console.print(
269
+ f"[green]+[/green] Action completed ({execution_time:.3f}s)"
270
+ )
271
+ else:
272
+ console.print(f"[red]x[/red] Action failed ({execution_time:.3f}s)")
273
+
274
+ # Update history
275
+ ACTION_HISTORY[-1]["success"] = success
276
+ ACTION_HISTORY[-1]["execution_time"] = execution_time
277
+ ACTION_HISTORY[-1]["result"] = result
278
+
279
+ # Display result
280
+ if success:
281
+ action_count += 1
282
+ if result is not None:
283
+ console.print(f" Result: {format_result(result)}")
284
+ else:
285
+ console.print(f"[red]{error_msg}[/red]")
286
+
287
+ console.print() # Extra newline for spacing
288
+
289
+ except KeyboardInterrupt:
290
+ console.print("\n\n[yellow]Session interrupted by user[/yellow]")
291
+ finally:
292
+ if SESSION_FILE.exists():
293
+ SESSION_FILE.unlink()
294
+
295
+ # Print summary
296
+ console.print("\n[bold]" + "=" * 80 + "[/bold]")
297
+ console.print("[bold]Session Summary[/bold]")
298
+ console.print("[bold]" + "=" * 80 + "[/bold]")
299
+ console.print(f" Total actions executed: {action_count}")
300
+ console.print(f" Session duration: {datetime.now() - session_start}")
301
+
302
+ if ACTION_HISTORY:
303
+ successful = sum(1 for a in ACTION_HISTORY if a.get("success"))
304
+ failed = len(ACTION_HISTORY) - successful
305
+ console.print(f" Successful: [green]{successful}[/green]")
306
+ console.print(f" Failed: [red]{failed}[/red]")
307
+
308
+ console.print("\n[blue]WebSocket connection closed[/blue]\n")
309
+
310
+
311
+ def main():
312
+ """Main entry point for the gui_test_session command"""
313
+ # Start interactive session
314
+ load_dotenv()
315
+ payload = ClerkCodePayload(
316
+ document=Document(id="test-session"),
317
+ structured_data={},
318
+ run_id="test-session-run",
319
+ )
320
+
321
+ # Show spinner while the decorator establishes WebSocket connection
322
+ with console.status("[dim]Waiting for tool to connect...", spinner="dots"):
323
+ start_interactive_session(payload)
324
+
325
+
326
+ if __name__ == "__main__":
327
+ main()