"""OpenClaw CLI client for managing cron jobs. Wraps `openclaw automations` commands to create, delete, and list cron jobs. """ import asyncio import json import logging from dataclasses import dataclass from typing import Optional _logger = logging.getLogger("agenteval") @dataclass class OpenClawCron: """OpenClaw cron job info.""" id: str name: str schedule: str enabled: bool state: Optional[dict] = None class OpenClawClient: """Client for OpenClaw CLI commands.""" def __init__(self, openclaw_bin: str = "openclaw"): self.openclaw_bin = openclaw_bin async def _run_command(self, *args: str) -> tuple[int, str, str]: """Run an OpenClaw CLI command. Returns: (returncode, stdout, stderr) """ cmd = [self.openclaw_bin] + list(args) _logger.debug(f"Running OpenClaw command: {' '.join(cmd)}") process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) stdout, stderr = await process.communicate() return ( process.returncode or 0, stdout.decode("utf-8"), stderr.decode("utf-8"), ) async def create_cron( self, *, name: str, schedule: str, skill: str, state: Optional[dict] = None, ) -> str: """Create a cron job. Args: name: Cron job name schedule: Cron schedule expression (e.g., "* * * * *") skill: Skill to execute state: Initial state (JSON) Returns: Cron job ID Raises: RuntimeError: If creation fails """ args = [ "automations", "create", schedule, f"--name={name}", f"--skill={skill}", ] if state: args.append(f"--state={json.dumps(state)}") returncode, stdout, stderr = await self._run_command(*args) if returncode != 0: raise RuntimeError(f"Failed to create cron: {stderr}") # Parse cron ID from output # Expected output format: "Created automation " lines = stdout.strip().split("\n") for line in lines: if "Created automation" in line: cron_id = line.split()[-1] _logger.info(f"Created OpenClaw cron {cron_id}: {name}") return cron_id raise RuntimeError(f"Failed to parse cron ID from output: {stdout}") async def delete_cron(self, cron_id: str) -> None: """Delete a cron job. Args: cron_id: Cron job ID Raises: RuntimeError: If deletion fails """ returncode, stdout, stderr = await self._run_command( "automations", "remove", cron_id, ) if returncode != 0: raise RuntimeError(f"Failed to delete cron {cron_id}: {stderr}") _logger.info(f"Deleted OpenClaw cron {cron_id}") async def list_crons(self) -> list[OpenClawCron]: """List all cron jobs. Returns: List of cron jobs Raises: RuntimeError: If listing fails """ returncode, stdout, stderr = await self._run_command( "automations", "list", "--json", ) if returncode != 0: raise RuntimeError(f"Failed to list crons: {stderr}") try: data = json.loads(stdout) crons = [] for item in data: crons.append( OpenClawCron( id=item["id"], name=item.get("name", ""), schedule=item.get("schedule", ""), enabled=item.get("enabled", True), state=item.get("state"), ) ) return crons except (json.JSONDecodeError, KeyError) as e: raise RuntimeError(f"Failed to parse cron list: {e}") from e async def get_cron_state(self, cron_id: str) -> Optional[dict]: """Get cron job state. Args: cron_id: Cron job ID Returns: State dict, or None if not found """ crons = await self.list_crons() for cron in crons: if cron.id == cron_id: return cron.state return None