droidrun 0.3.2__py3-none-any.whl → 0.3.3__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.
@@ -18,10 +18,24 @@ logger = logging.getLogger("droidrun")
18
18
 
19
19
  class Trajectory:
20
20
 
21
- def __init__(self):
22
- """Initializes an empty trajectory class."""
21
+ def __init__(self, goal: str = None):
22
+ """Initializes an empty trajectory class.
23
+
24
+ Args:
25
+ goal: The goal/prompt that this trajectory is trying to achieve
26
+ """
23
27
  self.events: List[Event] = []
24
28
  self.screenshots: List[bytes] = []
29
+ self.macro: List[Event] = []
30
+ self.goal = goal or "DroidRun automation sequence"
31
+
32
+ def set_goal(self, goal: str) -> None:
33
+ """Update the goal/description for this trajectory.
34
+
35
+ Args:
36
+ goal: The new goal/prompt description
37
+ """
38
+ self.goal = goal
25
39
 
26
40
 
27
41
  def create_screenshot_gif(self, output_path: str, duration: int = 1000) -> str:
@@ -33,9 +47,10 @@ class Trajectory:
33
47
  duration: Duration for each frame in milliseconds
34
48
 
35
49
  Returns:
36
- Path to the created GIF file
50
+ Path to the created GIF file, or None if no screenshots available
37
51
  """
38
52
  if len(self.screenshots) == 0:
53
+ logger.info("📷 No screenshots available for GIF creation")
39
54
  return None
40
55
 
41
56
  images = []
@@ -62,17 +77,20 @@ class Trajectory:
62
77
  ) -> str:
63
78
  """
64
79
  Save trajectory steps to a JSON file and create a GIF of screenshots if available.
80
+ Also saves the macro sequence as a separate file for replay.
81
+ Creates a dedicated folder for each trajectory containing all related files.
65
82
 
66
83
  Args:
67
- directory: Directory to save the trajectory files
84
+ directory: Base directory to save the trajectory files
68
85
 
69
86
  Returns:
70
- Path to the saved trajectory file
87
+ Path to the trajectory folder
71
88
  """
72
89
  os.makedirs(directory, exist_ok=True)
73
90
 
74
91
  timestamp = time.strftime("%Y%m%d_%H%M%S")
75
- base_path = os.path.join(directory, f"trajectory_{timestamp}")
92
+ trajectory_folder = os.path.join(directory, f"trajectory_{timestamp}")
93
+ os.makedirs(trajectory_folder, exist_ok=True)
76
94
 
77
95
  def make_serializable(obj):
78
96
  """Recursively make objects JSON serializable."""
@@ -100,6 +118,7 @@ class Trajectory:
100
118
  else:
101
119
  return obj
102
120
 
121
+ # Save main trajectory events
103
122
  serializable_events = []
104
123
  for event in self.events:
105
124
  event_dict = {
@@ -109,13 +128,209 @@ class Trajectory:
109
128
  }
110
129
  serializable_events.append(event_dict)
111
130
 
112
- json_path = f"{base_path}.json"
113
- with open(json_path, "w") as f:
131
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
132
+ with open(trajectory_json_path, "w") as f:
114
133
  json.dump(serializable_events, f, indent=2)
115
134
 
116
- self.create_screenshot_gif(base_path)
135
+ # Save macro sequence as a separate file for replay
136
+ if self.macro:
137
+ macro_data = []
138
+ for macro_event in self.macro:
139
+ macro_dict = {
140
+ "type": macro_event.__class__.__name__,
141
+ **{k: make_serializable(v) for k, v in macro_event.__dict__.items()
142
+ if not k.startswith('_')}
143
+ }
144
+ macro_data.append(macro_dict)
145
+
146
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
147
+ with open(macro_json_path, "w") as f:
148
+ json.dump({
149
+ "version": "1.0",
150
+ "description": self.goal,
151
+ "timestamp": timestamp,
152
+ "total_actions": len(macro_data),
153
+ "actions": macro_data
154
+ }, f, indent=2)
155
+
156
+ logger.info(f"💾 Saved macro sequence with {len(macro_data)} actions to {macro_json_path}")
157
+
158
+ # Create screenshot GIF
159
+ gif_path = self.create_screenshot_gif(os.path.join(trajectory_folder, "screenshots"))
160
+ if gif_path:
161
+ logger.info(f"🎬 Saved screenshot GIF to {gif_path}")
162
+
163
+ logger.info(f"📁 Trajectory saved to folder: {trajectory_folder}")
164
+ return trajectory_folder
165
+
166
+ @staticmethod
167
+ def load_trajectory_folder(trajectory_folder: str) -> Dict[str, Any]:
168
+ """
169
+ Load trajectory data from a trajectory folder.
170
+
171
+ Args:
172
+ trajectory_folder: Path to the trajectory folder
173
+
174
+ Returns:
175
+ Dictionary containing trajectory data, macro data, and file paths
176
+ """
177
+ result = {
178
+ "trajectory_data": None,
179
+ "macro_data": None,
180
+ "gif_path": None,
181
+ "folder_path": trajectory_folder
182
+ }
183
+
184
+ try:
185
+ # Load main trajectory
186
+ trajectory_json_path = os.path.join(trajectory_folder, "trajectory.json")
187
+ if os.path.exists(trajectory_json_path):
188
+ with open(trajectory_json_path, "r") as f:
189
+ result["trajectory_data"] = json.load(f)
190
+ logger.info(f"📖 Loaded trajectory data from {trajectory_json_path}")
191
+
192
+ # Load macro sequence
193
+ macro_json_path = os.path.join(trajectory_folder, "macro.json")
194
+ if os.path.exists(macro_json_path):
195
+ with open(macro_json_path, "r") as f:
196
+ result["macro_data"] = json.load(f)
197
+ logger.info(f"📖 Loaded macro data from {macro_json_path}")
198
+
199
+ # Check for GIF
200
+ gif_path = os.path.join(trajectory_folder, "screenshots.gif")
201
+ if os.path.exists(gif_path):
202
+ result["gif_path"] = gif_path
203
+ logger.info(f"🎬 Found screenshot GIF at {gif_path}")
204
+
205
+ return result
206
+
207
+ except Exception as e:
208
+ logger.error(f"❌ Error loading trajectory folder {trajectory_folder}: {e}")
209
+ return result
210
+
211
+ @staticmethod
212
+ def load_macro_sequence(macro_file_path: str) -> Dict[str, Any]:
213
+ """
214
+ Load a macro sequence from a saved macro file.
215
+
216
+ Args:
217
+ macro_file_path: Path to the macro JSON file (can be full path or trajectory folder)
218
+
219
+ Returns:
220
+ Dictionary containing the macro sequence data
221
+ """
222
+ # Check if it's a folder path - if so, look for macro.json inside
223
+ if os.path.isdir(macro_file_path):
224
+ macro_file_path = os.path.join(macro_file_path, "macro.json")
225
+
226
+ try:
227
+ with open(macro_file_path, "r") as f:
228
+ macro_data = json.load(f)
229
+
230
+ logger.info(f"📖 Loaded macro sequence with {macro_data.get('total_actions', 0)} actions from {macro_file_path}")
231
+ return macro_data
232
+ except FileNotFoundError:
233
+ logger.error(f"❌ Macro file not found: {macro_file_path}")
234
+ return {}
235
+ except json.JSONDecodeError as e:
236
+ logger.error(f"❌ Error parsing macro file {macro_file_path}: {e}")
237
+ return {}
238
+
239
+ @staticmethod
240
+ def get_macro_summary(macro_data: Dict[str, Any]) -> Dict[str, Any]:
241
+ """
242
+ Get a summary of a macro sequence.
243
+
244
+ Args:
245
+ macro_data: The macro data dictionary
246
+
247
+ Returns:
248
+ Dictionary with statistics about the macro
249
+ """
250
+ if not macro_data or "actions" not in macro_data:
251
+ return {"error": "Invalid macro data"}
252
+
253
+ actions = macro_data["actions"]
254
+
255
+ # Count action types
256
+ action_types = {}
257
+ for action in actions:
258
+ action_type = action.get("action_type", "unknown")
259
+ action_types[action_type] = action_types.get(action_type, 0) + 1
260
+
261
+ # Calculate duration if timestamps are available
262
+ timestamps = [action.get("timestamp") for action in actions if action.get("timestamp")]
263
+ duration = max(timestamps) - min(timestamps) if len(timestamps) > 1 else 0
264
+
265
+ return {
266
+ "version": macro_data.get("version", "unknown"),
267
+ "description": macro_data.get("description", "No description"),
268
+ "total_actions": len(actions),
269
+ "action_types": action_types,
270
+ "duration_seconds": round(duration, 2) if duration > 0 else None,
271
+ "timestamp": macro_data.get("timestamp", "unknown")
272
+ }
117
273
 
118
- return json_path
274
+ @staticmethod
275
+ def print_macro_summary(macro_file_path: str) -> None:
276
+ """
277
+ Print a summary of a macro sequence.
278
+
279
+ Args:
280
+ macro_file_path: Path to the macro JSON file or trajectory folder
281
+ """
282
+ macro_data = Trajectory.load_macro_sequence(macro_file_path)
283
+ if not macro_data:
284
+ print("❌ Could not load macro data")
285
+ return
286
+
287
+ summary = Trajectory.get_macro_summary(macro_data)
288
+
289
+ print("=== Macro Summary ===")
290
+ print(f"File: {macro_file_path}")
291
+ print(f"Version: {summary.get('version', 'unknown')}")
292
+ print(f"Description: {summary.get('description', 'No description')}")
293
+ print(f"Timestamp: {summary.get('timestamp', 'unknown')}")
294
+ print(f"Total actions: {summary.get('total_actions', 0)}")
295
+ if summary.get('duration_seconds'):
296
+ print(f"Duration: {summary['duration_seconds']} seconds")
297
+ print("Action breakdown:")
298
+ for action_type, count in summary.get('action_types', {}).items():
299
+ print(f" - {action_type}: {count}")
300
+ print("=====================")
301
+
302
+ @staticmethod
303
+ def print_trajectory_folder_summary(trajectory_folder: str) -> None:
304
+ """
305
+ Print a comprehensive summary of a trajectory folder.
306
+
307
+ Args:
308
+ trajectory_folder: Path to the trajectory folder
309
+ """
310
+ folder_data = Trajectory.load_trajectory_folder(trajectory_folder)
311
+
312
+ print("=== Trajectory Folder Summary ===")
313
+ print(f"Folder: {trajectory_folder}")
314
+ print(f"Trajectory data: {'✅ Available' if folder_data['trajectory_data'] else '❌ Missing'}")
315
+ print(f"Macro data: {'✅ Available' if folder_data['macro_data'] else '❌ Missing'}")
316
+ print(f"Screenshot GIF: {'✅ Available' if folder_data['gif_path'] else '❌ Missing'}")
317
+
318
+ if folder_data['macro_data']:
319
+ print("\n--- Macro Summary ---")
320
+ summary = Trajectory.get_macro_summary(folder_data['macro_data'])
321
+ print(f"Description: {summary.get('description', 'No description')}")
322
+ print(f"Total actions: {summary.get('total_actions', 0)}")
323
+ if summary.get('duration_seconds'):
324
+ print(f"Duration: {summary['duration_seconds']} seconds")
325
+ print("Action breakdown:")
326
+ for action_type, count in summary.get('action_types', {}).items():
327
+ print(f" - {action_type}: {count}")
328
+
329
+ if folder_data['trajectory_data']:
330
+ print(f"\n--- Trajectory Summary ---")
331
+ print(f"Total events: {len(folder_data['trajectory_data'])}")
332
+
333
+ print("=================================")
119
334
 
120
335
  def get_trajectory_statistics(trajectory_data: Dict[str, Any]) -> Dict[str, Any]:
121
336
  """
@@ -181,4 +396,36 @@ class Trajectory:
181
396
  print(f"Execution steps: {stats['execution_steps']}")
182
397
  print(f"Successful executions: {stats['successful_executions']}")
183
398
  print(f"Failed executions: {stats['failed_executions']}")
184
- print("==========================")
399
+ print("==========================")
400
+
401
+
402
+ # Example usage:
403
+ """
404
+ # Save a trajectory with a specific goal (automatically creates folder structure)
405
+ trajectory = Trajectory(goal="Open settings and check battery level")
406
+ # ... add events and screenshots to trajectory ...
407
+ folder_path = trajectory.save_trajectory()
408
+
409
+ # Or update the goal later
410
+ trajectory.set_goal("Navigate to Settings and find device info")
411
+
412
+ # Load entire trajectory folder
413
+ folder_data = Trajectory.load_trajectory_folder(folder_path)
414
+ trajectory_events = folder_data['trajectory_data']
415
+ macro_actions = folder_data['macro_data']
416
+ gif_path = folder_data['gif_path']
417
+
418
+ # Load just the macro from folder
419
+ macro_data = Trajectory.load_macro_sequence(folder_path)
420
+
421
+ # Print summaries
422
+ Trajectory.print_trajectory_folder_summary(folder_path)
423
+ Trajectory.print_macro_summary(folder_path)
424
+
425
+ # Example folder structure created:
426
+ # trajectories/
427
+ # └── trajectory_20250108_143052/
428
+ # ├── trajectory.json # Full trajectory events
429
+ # ├── macro.json # Macro sequence with goal as description
430
+ # └── screenshots.gif # Screenshot animation
431
+ """
droidrun/cli/main.py CHANGED
@@ -9,10 +9,11 @@ import logging
9
9
  import warnings
10
10
  from contextlib import nullcontext
11
11
  from rich.console import Console
12
+ from adbutils import adb
12
13
  from droidrun.agent.droid import DroidAgent
13
14
  from droidrun.agent.utils.llm_picker import load_llm
14
- from droidrun.adb import DeviceManager
15
15
  from droidrun.tools import AdbTools, IOSTools
16
+ from droidrun.agent.context.personas import DEFAULT, BIG_AGENT
16
17
  from functools import wraps
17
18
  from droidrun.cli.logs import LogHandler
18
19
  from droidrun.telemetry import print_telemetry_message
@@ -22,6 +23,7 @@ from droidrun.portal import (
22
23
  PORTAL_PACKAGE_NAME,
23
24
  ping_portal,
24
25
  )
26
+ from droidrun.macro.cli import macro_cli
25
27
 
26
28
  # Suppress all warnings
27
29
  warnings.filterwarnings("ignore")
@@ -29,7 +31,6 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
29
31
  os.environ["GRPC_ENABLE_FORK_SUPPORT"] = "false"
30
32
 
31
33
  console = Console()
32
- device_manager = DeviceManager()
33
34
 
34
35
 
35
36
  def configure_logging(goal: str, debug: bool):
@@ -38,7 +39,7 @@ def configure_logging(goal: str, debug: bool):
38
39
 
39
40
  handler = LogHandler(goal)
40
41
  handler.setFormatter(
41
- logging.Formatter("%(levelname)s %(message)s", "%H:%M:%S")
42
+ logging.Formatter("%(levelname)s %(name)s %(message)s", "%H:%M:%S")
42
43
  if debug
43
44
  else logging.Formatter("%(message)s", "%H:%M:%S")
44
45
  )
@@ -47,6 +48,12 @@ def configure_logging(goal: str, debug: bool):
47
48
  logger.setLevel(logging.DEBUG if debug else logging.INFO)
48
49
  logger.propagate = False
49
50
 
51
+ if debug:
52
+ tools_logger = logging.getLogger("droidrun-tools")
53
+ tools_logger.addHandler(handler)
54
+ tools_logger.propagate = False
55
+ tools_logger.setLevel(logging.DEBUG if debug else logging.INFO)
56
+
50
57
  return handler
51
58
 
52
59
 
@@ -71,9 +78,11 @@ async def run_command(
71
78
  reasoning: bool,
72
79
  reflection: bool,
73
80
  tracing: bool,
74
- debug: bool,
81
+ debug: bool,
82
+ use_tcp: bool,
75
83
  save_trajectory: bool = False,
76
84
  ios: bool = False,
85
+ allow_drag: bool = False,
77
86
  **kwargs,
78
87
  ):
79
88
  """Run a command on your Android device using natural language."""
@@ -95,8 +104,8 @@ async def run_command(
95
104
  # Device setup
96
105
  if device is None and not ios:
97
106
  logger.info("🔍 Finding connected device...")
98
- device_manager = DeviceManager()
99
- devices = await device_manager.list_devices()
107
+
108
+ devices = adb.list()
100
109
  if not devices:
101
110
  raise ValueError("No connected devices found.")
102
111
  device = devices[0].serial
@@ -108,7 +117,12 @@ async def run_command(
108
117
  else:
109
118
  logger.info(f"📱 Using device: {device}")
110
119
 
111
- tools = AdbTools(serial=device) if not ios else IOSTools(url=device)
120
+ tools = AdbTools(serial=device, use_tcp=use_tcp) if not ios else IOSTools(url=device)
121
+ # Set excluded tools based on CLI flags
122
+ excluded_tools = [] if allow_drag else ["drag"]
123
+
124
+ # Select personas based on --drag flag
125
+ personas = [BIG_AGENT] if allow_drag else [DEFAULT]
112
126
 
113
127
  # LLM setup
114
128
  log_handler.update_step("Initializing LLM...")
@@ -130,10 +144,13 @@ async def run_command(
130
144
  if tracing:
131
145
  logger.info("🔍 Tracing enabled")
132
146
 
147
+
133
148
  droid_agent = DroidAgent(
134
149
  goal=command,
135
150
  llm=llm,
136
151
  tools=tools,
152
+ personas=personas,
153
+ excluded_tools=excluded_tools,
137
154
  max_steps=steps,
138
155
  timeout=1000,
139
156
  vision=vision,
@@ -236,6 +253,9 @@ class DroidRunCLI(click.Group):
236
253
  @click.option(
237
254
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
238
255
  )
256
+ @click.option(
257
+ "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
258
+ )
239
259
  @click.option(
240
260
  "--save-trajectory",
241
261
  is_flag=True,
@@ -256,6 +276,7 @@ def cli(
256
276
  reflection: bool,
257
277
  tracing: bool,
258
278
  debug: bool,
279
+ use_tcp: bool,
259
280
  save_trajectory: bool,
260
281
  ):
261
282
  """DroidRun - Control your Android device through LLM agents."""
@@ -311,12 +332,22 @@ def cli(
311
332
  @click.option(
312
333
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
313
334
  )
335
+ @click.option(
336
+ "--use-tcp", is_flag=True, help="Use TCP communication for device control", default=False
337
+ )
314
338
  @click.option(
315
339
  "--save-trajectory",
316
340
  is_flag=True,
317
341
  help="Save agent trajectory to file",
318
342
  default=False,
319
343
  )
344
+ @click.option(
345
+ "--drag",
346
+ "allow_drag",
347
+ is_flag=True,
348
+ help="Enable drag tool",
349
+ default=False,
350
+ )
320
351
  @click.option("--ios", is_flag=True, help="Run on iOS device", default=False)
321
352
  def run(
322
353
  command: str,
@@ -332,7 +363,9 @@ def run(
332
363
  reflection: bool,
333
364
  tracing: bool,
334
365
  debug: bool,
366
+ use_tcp: bool,
335
367
  save_trajectory: bool,
368
+ allow_drag: bool,
336
369
  ios: bool,
337
370
  ):
338
371
  """Run a command on your Android device using natural language."""
@@ -350,18 +383,19 @@ def run(
350
383
  reflection,
351
384
  tracing,
352
385
  debug,
386
+ use_tcp,
353
387
  temperature=temperature,
354
388
  save_trajectory=save_trajectory,
389
+ allow_drag=allow_drag,
355
390
  ios=ios,
356
391
  )
357
392
 
358
393
 
359
394
  @cli.command()
360
- @coro
361
- async def devices():
395
+ def devices():
362
396
  """List connected Android devices."""
363
397
  try:
364
- devices = await device_manager.list_devices()
398
+ devices = adb.list()
365
399
  if not devices:
366
400
  console.print("[yellow]No devices connected.[/]")
367
401
  return
@@ -375,27 +409,24 @@ async def devices():
375
409
 
376
410
  @cli.command()
377
411
  @click.argument("serial")
378
- @click.option("--port", "-p", default=5555, help="ADB port (default: 5555)")
379
- @coro
380
- async def connect(serial: str, port: int):
412
+ def connect(serial: str):
381
413
  """Connect to a device over TCP/IP."""
382
414
  try:
383
- device = await device_manager.connect(serial, port)
384
- if device:
385
- console.print(f"[green]Successfully connected to {serial}:{port}[/]")
415
+ device = adb.connect(serial)
416
+ if device.count("already connected"):
417
+ console.print(f"[green]Successfully connected to {serial}[/]")
386
418
  else:
387
- console.print(f"[red]Failed to connect to {serial}:{port}[/]")
419
+ console.print(f"[red]Failed to connect to {serial}: {device}[/]")
388
420
  except Exception as e:
389
421
  console.print(f"[red]Error connecting to device: {e}[/]")
390
422
 
391
423
 
392
424
  @cli.command()
393
425
  @click.argument("serial")
394
- @coro
395
- async def disconnect(serial: str):
426
+ def disconnect(serial: str):
396
427
  """Disconnect from a device."""
397
428
  try:
398
- success = await device_manager.disconnect(serial)
429
+ success = adb.disconnect(serial, raise_error=True)
399
430
  if success:
400
431
  console.print(f"[green]Successfully disconnected from {serial}[/]")
401
432
  else:
@@ -410,12 +441,11 @@ async def disconnect(serial: str):
410
441
  @click.option(
411
442
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
412
443
  )
413
- @coro
414
- async def setup(path: str | None, device: str | None, debug: bool):
444
+ def setup(path: str | None, device: str | None, debug: bool):
415
445
  """Install and enable the DroidRun Portal on a device."""
416
446
  try:
417
447
  if not device:
418
- devices = await device_manager.list_devices()
448
+ devices = adb.list()
419
449
  if not devices:
420
450
  console.print("[yellow]No devices connected.[/]")
421
451
  return
@@ -423,7 +453,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
423
453
  device = devices[0].serial
424
454
  console.print(f"[blue]Using device:[/] {device}")
425
455
 
426
- device_obj = await device_manager.get_device(device)
456
+ device_obj = adb.device(device)
427
457
  if not device_obj:
428
458
  console.print(
429
459
  f"[bold red]Error:[/] Could not get device object for {device}"
@@ -443,18 +473,18 @@ async def setup(path: str | None, device: str | None, debug: bool):
443
473
  return
444
474
 
445
475
  console.print(f"[bold blue]Step 1/2: Installing APK:[/] {apk_path}")
446
- result = await device_obj.install_app(apk_path, True, True)
447
-
448
- if "Error" in result:
449
- console.print(f"[bold red]Installation failed:[/] {result}")
476
+ try:
477
+ device_obj.install(apk_path, uninstall=True, flags=["-g"], silent=not debug)
478
+ except Exception as e:
479
+ console.print(f"[bold red]Installation failed:[/] {e}")
450
480
  return
451
- else:
452
- console.print(f"[bold green]Installation successful![/]")
481
+
482
+ console.print(f"[bold green]Installation successful![/]")
453
483
 
454
484
  console.print(f"[bold blue]Step 2/2: Enabling accessibility service[/]")
455
485
 
456
486
  try:
457
- await enable_portal_accessibility(device_obj)
487
+ enable_portal_accessibility(device_obj)
458
488
 
459
489
  console.print("[green]Accessibility service enabled successfully![/]")
460
490
  console.print(
@@ -469,7 +499,7 @@ async def setup(path: str | None, device: str | None, debug: bool):
469
499
  "[yellow]Opening accessibility settings for manual configuration...[/]"
470
500
  )
471
501
 
472
- await device_obj.shell(
502
+ device_obj.shell(
473
503
  "am start -a android.settings.ACCESSIBILITY_SETTINGS"
474
504
  )
475
505
 
@@ -503,16 +533,15 @@ async def setup(path: str | None, device: str | None, debug: bool):
503
533
  @click.option(
504
534
  "--debug", is_flag=True, help="Enable verbose debug logging", default=False
505
535
  )
506
- @coro
507
- async def ping(device: str | None, debug: bool):
536
+ def ping(device: str | None, debug: bool):
508
537
  """Ping a device to check if it is ready and accessible."""
509
538
  try:
510
- device_obj = await device_manager.get_device(device)
539
+ device_obj = adb.device(device)
511
540
  if not device_obj:
512
541
  console.print(f"[bold red]Error:[/] Could not find device {device}")
513
542
  return
514
543
 
515
- await ping_portal(device_obj, debug)
544
+ ping_portal(device_obj, debug)
516
545
  console.print(
517
546
  "[bold green]Portal is installed and accessible. You're good to go![/]"
518
547
  )
@@ -524,6 +553,10 @@ async def ping(device: str | None, debug: bool):
524
553
  traceback.print_exc()
525
554
 
526
555
 
556
+ # Add macro commands as a subgroup
557
+ cli.add_command(macro_cli, name="macro")
558
+
559
+
527
560
  if __name__ == "__main__":
528
561
  command = "Open the settings app"
529
562
  device = None
@@ -537,9 +570,11 @@ if __name__ == "__main__":
537
570
  reflection = False
538
571
  tracing = True
539
572
  debug = True
573
+ use_tcp = True
540
574
  base_url = None
541
575
  api_base = None
542
576
  ios = False
577
+ allow_drag = False
543
578
  run_command(
544
579
  command=command,
545
580
  device=device,
@@ -552,8 +587,10 @@ if __name__ == "__main__":
552
587
  reflection=reflection,
553
588
  tracing=tracing,
554
589
  debug=debug,
590
+ use_tcp=use_tcp,
555
591
  base_url=base_url,
556
592
  api_base=api_base,
557
593
  api_key=api_key,
594
+ allow_drag=allow_drag,
558
595
  ios=ios,
559
596
  )
@@ -0,0 +1,14 @@
1
+ """
2
+ DroidRun Macro Module - Record and replay UI automation sequences.
3
+
4
+ This module provides functionality to replay macro sequences that were
5
+ recorded during DroidAgent execution.
6
+ """
7
+
8
+ from .replay import MacroPlayer, replay_macro_file, replay_macro_folder
9
+
10
+ __all__ = [
11
+ "MacroPlayer",
12
+ "replay_macro_file",
13
+ "replay_macro_folder"
14
+ ]
@@ -0,0 +1,10 @@
1
+ """
2
+ Entry point for running DroidRun macro CLI as a module.
3
+
4
+ Usage: python -m droidrun.macro <command>
5
+ """
6
+
7
+ from droidrun.macro.cli import macro_cli
8
+
9
+ if __name__ == "__main__":
10
+ macro_cli()