clerk-sdk 0.4.17__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.
clerk/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  from .client import Clerk
2
2
 
3
3
 
4
- __version__ = "0.4.17"
4
+ __version__ = "0.5.0"
File without changes
@@ -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()