droidrun 0.3.10.dev5__py3-none-any.whl → 0.3.10.dev6__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.
@@ -0,0 +1,27 @@
1
+ """
2
+ Abstract base class for app card providers.
3
+
4
+ Providers load app-specific instruction cards based on package names.
5
+ Supports multiple backends: local files, remote servers, or composite strategies.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+
10
+
11
+
12
+ class AppCardProvider(ABC):
13
+ """Abstract interface for loading app-specific instruction cards."""
14
+
15
+ @abstractmethod
16
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
17
+ """
18
+ Load app card for a given package asynchronously.
19
+
20
+ Args:
21
+ package_name: Android package name (e.g., "com.google.android.gm")
22
+ instruction: User's instruction/goal (optional context for server providers)
23
+
24
+ Returns:
25
+ App card content as string, or empty string if not found or on error
26
+ """
27
+ pass
@@ -0,0 +1,7 @@
1
+ """App card provider implementations."""
2
+
3
+ from droidrun.app_cards.providers.local_provider import LocalAppCardProvider
4
+ from droidrun.app_cards.providers.server_provider import ServerAppCardProvider
5
+ from droidrun.app_cards.providers.composite_provider import CompositeAppCardProvider
6
+
7
+ __all__ = ["LocalAppCardProvider", "ServerAppCardProvider", "CompositeAppCardProvider"]
@@ -0,0 +1,97 @@
1
+ """
2
+ Composite app card provider.
3
+
4
+ Tries server first, falls back to local if server fails or returns empty.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict
9
+
10
+ from droidrun.config_manager.app_card_provider import AppCardProvider
11
+ from droidrun.config_manager.providers.local_provider import LocalAppCardProvider
12
+ from droidrun.config_manager.providers.server_provider import ServerAppCardProvider
13
+
14
+ logger = logging.getLogger("droidrun")
15
+
16
+
17
+ class CompositeAppCardProvider(AppCardProvider):
18
+ """
19
+ Load app cards from server with local fallback.
20
+
21
+ Strategy:
22
+ 1. Try server first
23
+ 2. If server fails or returns empty, try local
24
+ 3. Return first non-empty result, or empty if both fail
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ server_url: str,
30
+ app_cards_dir: str = "config/app_cards",
31
+ server_timeout: float = 2.0,
32
+ server_max_retries: int = 2
33
+ ):
34
+ """
35
+ Initialize composite provider.
36
+
37
+ Args:
38
+ server_url: Base URL of the app card server
39
+ app_cards_dir: Directory containing local app_cards.json
40
+ server_timeout: Server request timeout in seconds
41
+ server_max_retries: Number of server retry attempts
42
+ """
43
+ self.server_provider = ServerAppCardProvider(
44
+ server_url=server_url,
45
+ timeout=server_timeout,
46
+ max_retries=server_max_retries
47
+ )
48
+ self.local_provider = LocalAppCardProvider(app_cards_dir=app_cards_dir)
49
+
50
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
51
+ """
52
+ Load app card with server-first, local-fallback strategy.
53
+
54
+ Args:
55
+ package_name: Android package name (e.g., "com.google.android.gm")
56
+ instruction: User instruction/goal
57
+
58
+ Returns:
59
+ App card content from server or local, or empty string if both fail
60
+ """
61
+ if not package_name:
62
+ return ""
63
+
64
+ # Try server first
65
+ server_result = await self.server_provider.load_app_card(package_name, instruction)
66
+
67
+ if server_result:
68
+ return server_result
69
+
70
+ # Server failed or returned empty, try local
71
+ logger.debug(f"Composite provider: falling back to local for {package_name}")
72
+ local_result = await self.local_provider.load_app_card(package_name, instruction)
73
+
74
+ if local_result:
75
+ logger.info(f"Composite provider: using local fallback for {package_name}")
76
+ else:
77
+ logger.debug(f"Composite provider: no app card found for {package_name}")
78
+
79
+ return local_result
80
+
81
+ def clear_cache(self) -> None:
82
+ """Clear caches in both providers."""
83
+ self.server_provider.clear_cache()
84
+ self.local_provider.clear_cache()
85
+ logger.debug("Composite app card cache cleared")
86
+
87
+ def get_cache_stats(self) -> Dict[str, any]:
88
+ """
89
+ Get cache statistics from both providers.
90
+
91
+ Returns:
92
+ Dict with cache stats from server and local providers
93
+ """
94
+ return {
95
+ "server": self.server_provider.get_cache_stats(),
96
+ "local": self.local_provider.get_cache_stats(),
97
+ }
@@ -0,0 +1,116 @@
1
+ """
2
+ Local file-based app card provider.
3
+
4
+ Loads app cards from local filesystem using app_cards.json mapping.
5
+ """
6
+
7
+ import json
8
+ import logging
9
+ from pathlib import Path
10
+ from typing import Dict
11
+
12
+ from droidrun.config_manager.app_card_provider import AppCardProvider
13
+ from droidrun.config_manager.path_resolver import PathResolver
14
+
15
+ logger = logging.getLogger("droidrun")
16
+
17
+
18
+ class LocalAppCardProvider(AppCardProvider):
19
+ """Load app cards from local filesystem with in-memory caching."""
20
+
21
+ def __init__(self, app_cards_dir: str = "config/app_cards"):
22
+ """
23
+ Initialize local provider.
24
+
25
+ Args:
26
+ app_cards_dir: Directory containing app_cards.json and markdown files
27
+ """
28
+ # Resolve app_cards.json path once
29
+ mapping_path = PathResolver.resolve(f"{app_cards_dir}/app_cards.json")
30
+ self.app_cards_dir = mapping_path.parent
31
+
32
+ # Load mapping immediately
33
+ try:
34
+ if mapping_path.exists():
35
+ with open(mapping_path, "r", encoding="utf-8") as f:
36
+ self.mapping = json.load(f)
37
+ logger.debug(f"Loaded app_cards.json with {len(self.mapping)} entries")
38
+ else:
39
+ logger.warning(f"app_cards.json not found at {mapping_path}")
40
+ self.mapping = {}
41
+ except Exception as e:
42
+ logger.warning(f"Failed to load app_cards.json: {e}")
43
+ self.mapping = {}
44
+
45
+ # Content cache: (package_name, instruction) -> content
46
+ self._content_cache: Dict[tuple[str, str], str] = {}
47
+
48
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
49
+ """
50
+ Load app card for a package name from local files.
51
+
52
+ Args:
53
+ package_name: Android package name (e.g., "com.google.android.gm")
54
+ instruction: User instruction (for cache key consistency, not used in loading)
55
+
56
+ Returns:
57
+ App card content or empty string if not found
58
+ """
59
+ if not package_name:
60
+ return ""
61
+
62
+ # Check content cache first
63
+ cache_key = (package_name, instruction)
64
+ if cache_key in self._content_cache:
65
+ logger.debug(f"App card cache hit: {package_name}")
66
+ return self._content_cache[cache_key]
67
+
68
+ # Check if package exists in mapping
69
+ if package_name not in self.mapping:
70
+ self._content_cache[cache_key] = ""
71
+ return ""
72
+
73
+ # Get app card file path (relative to app_cards_dir)
74
+ filename = self.mapping[package_name]
75
+ app_card_path = self.app_cards_dir / filename
76
+
77
+ # Read file
78
+ try:
79
+ if not app_card_path.exists():
80
+ self._content_cache[cache_key] = ""
81
+ logger.debug(f"App card not found: {app_card_path}")
82
+ return ""
83
+
84
+ # Async file read
85
+ import asyncio
86
+ loop = asyncio.get_event_loop()
87
+ content = await loop.run_in_executor(
88
+ None, app_card_path.read_text, "utf-8"
89
+ )
90
+
91
+ # Cache and return
92
+ self._content_cache[cache_key] = content
93
+ logger.info(f"Loaded app card for {package_name} from {app_card_path}")
94
+ return content
95
+
96
+ except Exception as e:
97
+ logger.warning(f"Failed to load app card for {package_name}: {e}")
98
+ self._content_cache[cache_key] = ""
99
+ return ""
100
+
101
+ def clear_cache(self) -> None:
102
+ """Clear content cache (useful for testing or runtime reloading)."""
103
+ self._content_cache.clear()
104
+ logger.debug("Local app card cache cleared")
105
+
106
+ def get_cache_stats(self) -> Dict[str, int]:
107
+ """
108
+ Get cache statistics.
109
+
110
+ Returns:
111
+ Dict with cache stats (useful for debugging)
112
+ """
113
+ return {
114
+ "content_entries": len(self._content_cache),
115
+ "mapping_entries": len(self.mapping),
116
+ }
@@ -0,0 +1,126 @@
1
+ """
2
+ Server-based app card provider.
3
+
4
+ Fetches app cards from a remote HTTP server.
5
+ """
6
+
7
+ import logging
8
+ from typing import Dict
9
+
10
+ import httpx
11
+
12
+ from droidrun.config_manager.app_card_provider import AppCardProvider
13
+
14
+ logger = logging.getLogger("droidrun")
15
+
16
+
17
+ class ServerAppCardProvider(AppCardProvider):
18
+ """Load app cards from remote server with in-memory caching."""
19
+
20
+ def __init__(
21
+ self,
22
+ server_url: str,
23
+ timeout: float = 2.0,
24
+ max_retries: int = 2
25
+ ):
26
+ """
27
+ Initialize server provider.
28
+
29
+ Args:
30
+ server_url: Base URL of the app card server (e.g., "https://api.example.com")
31
+ timeout: Request timeout in seconds
32
+ max_retries: Number of retry attempts on failure
33
+ """
34
+ self.server_url = server_url.rstrip("/")
35
+ self.timeout = timeout
36
+ self.max_retries = max_retries
37
+ self._content_cache: Dict[tuple[str, str], str] = {}
38
+
39
+ async def load_app_card(self, package_name: str, instruction: str = "") -> str:
40
+ """
41
+ Load app card from remote server.
42
+
43
+ Args:
44
+ package_name: Android package name (e.g., "com.google.android.gm")
45
+ instruction: User instruction/goal (sent to server for context)
46
+
47
+ Returns:
48
+ App card content or empty string if not found or on error
49
+ """
50
+ if not package_name:
51
+ return ""
52
+
53
+ # Check content cache first (key: package_name, instruction)
54
+ cache_key = (package_name, instruction)
55
+ if cache_key in self._content_cache:
56
+ return self._content_cache[cache_key]
57
+
58
+ # Make HTTP request with retries
59
+ endpoint = f"{self.server_url}/app-cards"
60
+ payload = {
61
+ "package_name": package_name,
62
+ "instruction": instruction
63
+ }
64
+
65
+ for attempt in range(1, self.max_retries + 1):
66
+ try:
67
+ async with httpx.AsyncClient(timeout=self.timeout) as client:
68
+ response = await client.post(endpoint, json=payload)
69
+
70
+ if response.status_code == 200:
71
+ data = response.json()
72
+ app_card = data.get("app_card", "")
73
+
74
+ # Cache the result (even if empty)
75
+ self._content_cache[cache_key] = app_card
76
+ return app_card
77
+
78
+ elif response.status_code == 404:
79
+ # Not found is expected, cache empty result
80
+ self._content_cache[cache_key] = ""
81
+ return ""
82
+
83
+ else:
84
+ logger.warning(
85
+ f"Server returned status {response.status_code} for {package_name} "
86
+ f"(attempt {attempt}/{self.max_retries})"
87
+ )
88
+
89
+ except httpx.TimeoutException:
90
+ logger.warning(
91
+ f"Server request timeout for {package_name} "
92
+ f"(attempt {attempt}/{self.max_retries})"
93
+ )
94
+
95
+ except httpx.RequestError as e:
96
+ logger.warning(
97
+ f"Server request failed for {package_name}: {e} "
98
+ f"(attempt {attempt}/{self.max_retries})"
99
+ )
100
+
101
+ except Exception as e:
102
+ logger.warning(
103
+ f"Unexpected error loading app card from server: {e} "
104
+ f"(attempt {attempt}/{self.max_retries})"
105
+ )
106
+
107
+ # All retries failed, cache empty result
108
+ logger.warning(f"Failed to load app card from server after {self.max_retries} attempts")
109
+ self._content_cache[cache_key] = ""
110
+ return ""
111
+
112
+ def clear_cache(self) -> None:
113
+ """Clear content cache."""
114
+ self._content_cache.clear()
115
+ logger.debug("Server app card cache cleared")
116
+
117
+ def get_cache_stats(self) -> Dict[str, int]:
118
+ """
119
+ Get cache statistics.
120
+
121
+ Returns:
122
+ Dict with cache stats (useful for debugging)
123
+ """
124
+ return {
125
+ "content_entries": len(self._content_cache),
126
+ }
droidrun/cli/main.py CHANGED
@@ -8,14 +8,13 @@ import os
8
8
  import warnings
9
9
  from contextlib import nullcontext
10
10
  from functools import wraps
11
- from pathlib import Path
12
11
 
13
12
  import click
14
13
  from adbutils import adb
15
14
  from rich.console import Console
16
15
 
17
16
  from droidrun.agent.droid import DroidAgent
18
- from droidrun.agent.utils.llm_picker import load_llm
17
+ from droidrun.agent.utils.llm_picker import load_llm, load_llms_from_profiles
19
18
  from droidrun.cli.logs import LogHandler
20
19
  from droidrun.config_manager.config_manager import (
21
20
  AgentConfig,
@@ -240,7 +239,7 @@ async def run_command(
240
239
  if temperature is not None:
241
240
  overrides = {name: {'temperature': temperature} for name in profile_names}
242
241
 
243
- llms = config.load_all_llms(profile_names=profile_names, **overrides)
242
+ llms = load_llms_from_profiles(config.llm_profiles, profile_names=profile_names, **overrides)
244
243
  logger.info(f"🧠 Loaded {len(llms)} agent-specific LLMs from profiles")
245
244
 
246
245
  # ================================================================
@@ -532,29 +531,43 @@ def run(
532
531
  ):
533
532
  """Run a command on your Android device using natural language."""
534
533
 
535
- # Call our standalone function
536
- return run_command(
537
- command,
538
- config,
539
- device,
540
- provider,
541
- model,
542
- steps,
543
- base_url,
544
- api_base,
545
- vision,
546
- manager_vision,
547
- executor_vision,
548
- codeact_vision,
549
- reasoning,
550
- tracing,
551
- debug,
552
- use_tcp,
553
- temperature=temperature,
554
- save_trajectory=save_trajectory,
555
- allow_drag=allow_drag,
556
- ios=ios if ios is not None else False,
557
- )
534
+ try:
535
+ run_command(
536
+ command,
537
+ config,
538
+ device,
539
+ provider,
540
+ model,
541
+ steps,
542
+ base_url,
543
+ api_base,
544
+ vision,
545
+ manager_vision,
546
+ executor_vision,
547
+ codeact_vision,
548
+ reasoning,
549
+ tracing,
550
+ debug,
551
+ use_tcp,
552
+ temperature=temperature,
553
+ save_trajectory=save_trajectory,
554
+ allow_drag=allow_drag,
555
+ ios=ios if ios is not None else False,
556
+ )
557
+ finally:
558
+ # Disable DroidRun keyboard after execution
559
+ try:
560
+ if not (ios if ios is not None else False):
561
+ device_serial = adb.device().serial
562
+ if device_serial:
563
+ tools = AdbTools(serial=device, use_tcp=use_tcp if use_tcp is not None else False)
564
+ if hasattr(tools, 'device') and tools.device:
565
+ tools.device.shell("ime disable com.droidrun.portal/.DroidrunKeyboardIME")
566
+ click.echo("DroidRun keyboard disabled successfully")
567
+ # Cleanup tools
568
+ del tools
569
+ except Exception as disable_e:
570
+ click.echo(f"Warning: Failed to disable DroidRun keyboard: {disable_e}")
558
571
 
559
572
 
560
573
  @cli.command()
@@ -756,8 +769,206 @@ def ping(device: str | None, use_tcp: bool, debug: bool):
756
769
  cli.add_command(macro_cli, name="macro")
757
770
 
758
771
 
772
+ async def test(command: str):
773
+ config = ConfigManager(path="config.yaml")
774
+ # Initialize logging first (use config default if debug not specified)
775
+ debug_mode = debug if debug is not None else config.logging.debug
776
+ log_handler = configure_logging(command, debug_mode, config.logging.rich_text)
777
+ logger = logging.getLogger("droidrun")
778
+
779
+ log_handler.update_step("Initializing...")
780
+
781
+ with log_handler.render():
782
+ try:
783
+ logger.info(f"🚀 Starting: {command}")
784
+ print_telemetry_message()
785
+
786
+ # ================================================================
787
+ # STEP 1: Build config objects with CLI overrides
788
+ # ================================================================
789
+
790
+ # Build agent-specific configs with vision overrides
791
+ if vision is not None:
792
+ # --vision flag overrides all agents
793
+ manager_vision_val = vision
794
+ executor_vision_val = vision
795
+ codeact_vision_val = vision
796
+ logger.debug(f"CLI override: vision={vision} (all agents)")
797
+ else:
798
+ # Use individual overrides or config defaults
799
+ manager_vision_val = config.agent.manager.vision
800
+ executor_vision_val = config.agent.executor.vision
801
+ codeact_vision_val = config.agent.codeact.vision
802
+
803
+ manager_cfg = ManagerConfig(
804
+ vision=manager_vision_val,
805
+ system_prompt="rev1.jinja2"
806
+ )
807
+
808
+ executor_cfg = ExecutorConfig(
809
+ vision=executor_vision_val,
810
+ system_prompt="rev1.jinja2"
811
+ )
812
+
813
+ codeact_cfg = CodeActConfig(
814
+ vision=codeact_vision_val,
815
+ system_prompt=config.agent.codeact.system_prompt,
816
+ user_prompt=config.agent.codeact.user_prompt
817
+ )
818
+
819
+ agent_cfg = AgentConfig(
820
+ max_steps=steps if steps is not None else config.agent.max_steps,
821
+ reasoning=reasoning if reasoning is not None else config.agent.reasoning,
822
+ after_sleep_action=config.agent.after_sleep_action,
823
+ wait_for_stable_ui=config.agent.wait_for_stable_ui,
824
+ prompts_dir=config.agent.prompts_dir,
825
+ manager=manager_cfg,
826
+ executor=executor_cfg,
827
+ codeact=codeact_cfg,
828
+ app_cards=config.agent.app_cards,
829
+ )
830
+
831
+ device_cfg = DeviceConfig(
832
+ serial=device if device is not None else config.device.serial,
833
+ use_tcp=use_tcp if use_tcp is not None else config.device.use_tcp,
834
+ )
835
+
836
+ tools_cfg = ToolsConfig(
837
+ allow_drag=allow_drag if allow_drag is not None else config.tools.allow_drag,
838
+ )
839
+
840
+ logging_cfg = LoggingConfig(
841
+ debug=debug if debug is not None else config.logging.debug,
842
+ save_trajectory=save_trajectory if save_trajectory is not None else config.logging.save_trajectory,
843
+ rich_text=config.logging.rich_text,
844
+ )
845
+
846
+ tracing_cfg = TracingConfig(
847
+ enabled=tracing if tracing is not None else config.tracing.enabled,
848
+ )
849
+
850
+ # ================================================================
851
+ # STEP 3: Load LLMs
852
+ # ================================================================
853
+
854
+ log_handler.update_step("Loading LLMs...")
855
+
856
+ # No custom provider/model - use profiles from config
857
+ logger.info("📋 Loading LLMs from config profiles...")
858
+
859
+ profile_names = ['manager', 'executor', 'codeact', 'text_manipulator', 'app_opener']
860
+
861
+ # Apply temperature override to all profiles if specified
862
+ overrides = {}
863
+ if temperature is not None:
864
+ overrides = {name: {'temperature': temperature} for name in profile_names}
865
+
866
+ llms = load_llms_from_profiles(config.llm_profiles, profile_names=profile_names, **overrides)
867
+ logger.info(f"🧠 Loaded {len(llms)} agent-specific LLMs from profiles")
868
+
869
+ # ================================================================
870
+ # STEP 4: Setup device and tools
871
+ # ================================================================
872
+
873
+ log_handler.update_step("Setting up tools...")
874
+
875
+ device_serial = device_cfg.serial
876
+ if device_serial is None and not ios:
877
+ logger.info("🔍 Finding connected device...")
878
+ devices = adb.list()
879
+ if not devices:
880
+ raise ValueError("No connected devices found.")
881
+ device_serial = devices[0].serial
882
+ device_cfg = DeviceConfig(serial=device_serial, use_tcp=device_cfg.use_tcp)
883
+ logger.info(f"📱 Using device: {device_serial}")
884
+ elif device_serial is None and ios:
885
+ raise ValueError("iOS device not specified. Please specify device base url via --device")
886
+ else:
887
+ logger.info(f"📱 Using device: {device_serial}")
888
+
889
+ tools = (
890
+ AdbTools(
891
+ serial=device_serial,
892
+ use_tcp=device_cfg.use_tcp,
893
+ app_opener_llm=llms.get('app_opener'),
894
+ text_manipulator_llm=llms.get('text_manipulator')
895
+ )
896
+ if not ios
897
+ else IOSTools(url=device_serial)
898
+ )
899
+
900
+ excluded_tools = [] if tools_cfg.allow_drag else ["drag"]
901
+
902
+ # ================================================================
903
+ # STEP 5: Initialize DroidAgent with all settings
904
+ # ================================================================
905
+
906
+ log_handler.update_step("Initializing DroidAgent...")
907
+
908
+ mode = "planning with reasoning" if agent_cfg.reasoning else "direct execution"
909
+ logger.info(f"🤖 Agent mode: {mode}")
910
+ logger.info(f"👁️ Vision settings: Manager={agent_cfg.manager.vision}, "
911
+ f"Executor={agent_cfg.executor.vision}, CodeAct={agent_cfg.codeact.vision}")
912
+
913
+ if tracing_cfg.enabled:
914
+ logger.info("🔍 Tracing enabled")
915
+
916
+ droid_agent = DroidAgent(
917
+ goal=command,
918
+ llms=llms,
919
+ tools=tools,
920
+ config=config,
921
+ agent_config=agent_cfg,
922
+ device_config=device_cfg,
923
+ tools_config=tools_cfg,
924
+ logging_config=logging_cfg,
925
+ tracing_config=tracing_cfg,
926
+ excluded_tools=excluded_tools,
927
+ timeout=1000,
928
+ )
929
+
930
+ # ================================================================
931
+ # STEP 6: Run agent
932
+ # ================================================================
933
+
934
+ logger.info("▶️ Starting agent execution...")
935
+ logger.info("Press Ctrl+C to stop")
936
+ log_handler.update_step("Running agent...")
937
+
938
+ try:
939
+ handler = droid_agent.run()
940
+
941
+ async for event in handler.stream_events():
942
+ log_handler.handle_event(event)
943
+ result = await handler # noqa: F841
944
+
945
+ except KeyboardInterrupt:
946
+ log_handler.is_completed = True
947
+ log_handler.is_success = False
948
+ log_handler.current_step = "Stopped by user"
949
+ logger.info("⏹️ Stopped by user")
950
+
951
+ except Exception as e:
952
+ log_handler.is_completed = True
953
+ log_handler.is_success = False
954
+ log_handler.current_step = f"Error: {e}"
955
+ logger.error(f"💥 Error: {e}")
956
+ if logging_cfg.debug:
957
+ import traceback
958
+ logger.debug(traceback.format_exc())
959
+
960
+ except Exception as e:
961
+ log_handler.current_step = f"Error: {e}"
962
+ logger.error(f"💥 Setup error: {e}")
963
+ debug_mode = debug if debug is not None else config.logging.debug
964
+ if debug_mode:
965
+ import traceback
966
+ logger.debug(traceback.format_exc())
967
+
968
+
969
+
759
970
  if __name__ == "__main__":
760
- command = "Download clash royale app"
971
+ command = "set gboard to the default keyboard"
761
972
  device = None
762
973
  provider = "GoogleGenAI"
763
974
  model = "models/gemini-2.5-flash"
@@ -765,7 +976,7 @@ if __name__ == "__main__":
765
976
  api_key = os.getenv("GOOGLE_API_KEY")
766
977
  steps = 15
767
978
  vision = True
768
- reasoning = True
979
+ reasoning = False
769
980
  tracing = True
770
981
  debug = True
771
982
  use_tcp = False
@@ -774,6 +985,6 @@ if __name__ == "__main__":
774
985
  ios = False
775
986
  save_trajectory = "none"
776
987
  allow_drag = False
777
- run_command(
778
- command
988
+ asyncio.run(
989
+ test(command)
779
990
  )