rsclaw 0.0.1-alpha.1

rsclaw: High-performance AI agent (BETA). Optimized for M4 Max and 2GB VPS. 100% compatible with openclaw
Documentation
use chrono::{DateTime, Datelike, Local, Timelike};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

/// Cron task type.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskType {
    Agent,
    Skill,
    Command,
}

/// Task status.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskStatus {
    Pending,
    Running,
    Completed,
    Failed,
}

/// Cron task definition.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CronTask {
    pub id: Arc<str>,
    pub expression: Arc<str>,
    pub task_type: TaskType,
    pub target: Arc<str>,
    pub description: Arc<str>,
    pub status: TaskStatus,
    pub retry_count: u32,
    pub max_retries: u32,
    pub timeout_secs: u64,
    pub created_at: u64,
    pub last_run: Option<u64>,
    pub next_run: Option<u64>,
    pub last_error: Option<Arc<str>>,
}

impl CronTask {
    /// Create a new cron task.
    pub fn new(
        expression: Arc<str>,
        task_type: TaskType,
        target: Arc<str>,
        description: Arc<str>,
    ) -> Self {
        let now = std::time::SystemTime::now()
            .duration_since(std::time::SystemTime::UNIX_EPOCH)
            .unwrap_or_default()
            .as_secs();

        Self {
            id: Arc::from(uuid::Uuid::new_v4().to_string()),
            expression,
            task_type,
            target,
            description,
            status: TaskStatus::Pending,
            retry_count: 0,
            max_retries: 3,
            timeout_secs: 30,
            created_at: now,
            last_run: None,
            next_run: None,
            last_error: None,
        }
    }
}

/// Simple cron expression parser.
pub struct CronExpression {
    pub minutes: Vec<u8>,
    pub hours: Vec<u8>,
    pub day_of_month: Vec<u8>,
    pub month: Vec<u8>,
    pub day_of_week: Vec<u8>,
}

impl CronExpression {
    /// Parse a cron expression string.
    pub fn parse(expr: &str) -> Result<Self, String> {
        let parts: Vec<&str> = expr.split_whitespace().collect();

        if parts.len() != 5 {
            return Err(format!(
                "Invalid cron expression: expected 5 parts, got {}",
                parts.len()
            ));
        }

        Ok(Self {
            minutes: Self::parse_field(parts[0], 0, 59)?,
            hours: Self::parse_field(parts[1], 0, 23)?,
            day_of_month: Self::parse_field(parts[2], 1, 31)?,
            month: Self::parse_field(parts[3], 1, 12)?,
            day_of_week: Self::parse_field(parts[4], 0, 7)?,
        })
    }

    /// Parse a single cron field.
    fn parse_field(field: &str, min: u8, max: u8) -> Result<Vec<u8>, String> {
        let mut values = Vec::new();

        if field == "*" {
            for i in min..=max {
                values.push(i);
            }
            return Ok(values);
        }

        // Handle */N syntax
        if field.starts_with("*/") {
            let step: u8 = field[2..]
                .parse()
                .map_err(|_| format!("Invalid step value: {}", field))?;
            if step == 0 {
                return Err("Step cannot be zero".to_string());
            }
            let mut i = min;
            while i <= max {
                values.push(i);
                i += step;
            }
            return Ok(values);
        }

        // Handle ranges like 1-5
        if field.contains('-') {
            let parts: Vec<&str> = field.split('-').collect();
            if parts.len() != 2 {
                return Err(format!("Invalid range: {}", field));
            }
            let start: u8 = parts[0]
                .parse()
                .map_err(|_| format!("Invalid range start: {}", parts[0]))?;
            let end: u8 = parts[1]
                .parse()
                .map_err(|_| format!("Invalid range end: {}", parts[1]))?;
            if start > end || start < min || end > max {
                return Err(format!("Invalid range: {}-{}", start, end));
            }
            for i in start..=end {
                values.push(i);
            }
            return Ok(values);
        }

        // Handle comma-separated values
        for part in field.split(',') {
            let value: u8 = part
                .parse()
                .map_err(|_| format!("Invalid value: {}", part))?;
            if value < min || value > max {
                return Err(format!(
                    "Value out of range: {} (expected {}-{})",
                    value, min, max
                ));
            }
            values.push(value);
        }

        values.sort();
        values.dedup();
        Ok(values)
    }

    /// Check if current time matches this expression.
    pub fn matches(&self, datetime: &chrono::DateTime<chrono::Local>) -> bool {
        self.minutes.contains(&(datetime.minute() as u8))
            && self.hours.contains(&(datetime.hour() as u8))
            && self.day_of_month.contains(&(datetime.day() as u8))
            && self.month.contains(&(datetime.month() as u8))
            && self
                .day_of_week
                .contains(&(datetime.weekday().num_days_from_sunday() as u8))
    }
}