Phy Cron Explainer

ClawSkills 作者 phy041 v1.0.0

源码 ↗

安装 / 下载方式

TotalClaw CLI推荐
totalclaw install clawskills:phy041~phy-cron-explainer
cURL直接下载,无需登录
curl -fsSL https://skills.taituai.com/api/skills/clawskills%3Aphy041~phy-cron-explainer/file -o phy-cron-explainer.md
Git 仓库获取源码
git clone https://github.com/openclaw/skills/commit/bc809f995e64b79ce05b5c106095661309af6682
---
name: phy-cron-explainer
description: Cron expression explainer, validator, and converter. Translates any cron expression into plain English ("0 */6 * * *" → "Every 6 hours, at minute 0"), shows the next 5 scheduled run times with exact dates, detects impossible/conflicting expressions (Feb 30, conflicting day-of-week + day-of-month), identifies suspicious patterns (every minute in production, very-frequent schedules on expensive operations), and converts plain English descriptions to cron ("every weekday at 9am" → "0 9 * * 1-5"). Supports all major flavors: standard Unix 5-field, 6-field with seconds, AWS EventBridge, GitHub Actions schedule, Kubernetes CronJob, Spring @Scheduled, and special strings (@yearly, @monthly, @daily, @hourly, @reboot). Also scans .github/workflows/ and Kubernetes YAML files for cron schedules and audits them. Zero external API — pure local parsing. Triggers on "cron expression", "what does this cron mean", "cron to English", "cron schedule", "explain cron", "@reboot", "/cron".
license: Apache-2.0
metadata:
  author: PHY041
  version: "1.0.0"
  tags:
    - cron
    - scheduling
    - devops
    - developer-tools
    - kubernetes
    - github-actions
    - aws
    - automation
    - ci-cd
    - utilities
---

# Cron Explainer

`0 2 * * 1`

What does that mean? Every Monday at 2am. How about `*/5 9-17 * * 1-5`? Every 5 minutes between 9am and 5pm on weekdays.

You shouldn't need to count fields in your head. This skill translates any cron expression into plain English, shows the next scheduled runs, catches impossible expressions before they go to production, converts English descriptions to cron, and scans your CI/K8s configs to audit all your schedules in one shot.

**All major cron flavors. Zero external API.**

---

## Trigger Phrases

- "cron expression", "explain this cron", "what does cron mean"
- "cron to English", "translate cron"
- "cron schedule", "next run time"
- "English to cron", "convert to cron"
- "cron syntax", "cron debug"
- "kubernetes cronjob", "github actions schedule"
- "/cron"

---

## How to Provide Input

```bash
# Option 1: Explain a cron expression
/cron "0 2 * * 1"
/cron "*/5 9-17 * * 1-5"
/cron "0 0 1,15 * *"

# Option 2: Explain a special string
/cron "@daily"
/cron "@reboot"
/cron "@every 6h"    # Kubernetes/Go format

# Option 3: Convert English to cron
/cron "every weekday at 9am"
/cron "every 5 minutes during business hours"
/cron "first day of every month at midnight"
/cron "every 15 minutes from 8am to 6pm on weekdays"

# Option 4: Show next N run times
/cron "0 */6 * * *" --next 10

# Option 5: Validate an expression
/cron "0 25 * * *" --validate    # 25th hour — invalid

# Option 6: Scan project files for cron schedules
/cron --scan .                    # scans GitHub Actions, Kubernetes YAML, crontab
/cron --scan .github/workflows/

# Option 7: Detect flavor
/cron "0/5 * * * ? *" --flavor aws-eventbridge
```

---

## Step 1: Parse and Explain Cron Expression

```python
from datetime import datetime, timedelta
import re
from typing import Optional


# ── Special strings ──────────────────────────────────────────────────────────

SPECIAL_STRINGS = {
    '@yearly':   ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@annually': ('0 0 1 1 *',  'Once a year, at midnight on January 1st'),
    '@monthly':  ('0 0 1 * *',  'Once a month, at midnight on the 1st'),
    '@weekly':   ('0 0 * * 0',  'Once a week, at midnight on Sunday'),
    '@daily':    ('0 0 * * *',  'Every day at midnight'),
    '@midnight': ('0 0 * * *',  'Every day at midnight'),
    '@hourly':   ('0 * * * *',  'Every hour, at the top of the hour'),
    '@reboot':   (None,         'Once, on system startup/reboot'),
    '@every_1m': ('* * * * *',  'Every minute'),
    '@every_5m': ('*/5 * * * *','Every 5 minutes'),
    '@every_1h': ('0 * * * *',  'Every hour'),
}

WEEKDAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
                 'Thursday', 'Friday', 'Saturday']
MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June',
               'July', 'August', 'September', 'October', 'November', 'December']


def detect_flavor(expression: str) -> str:
    """Detect cron flavor from expression structure."""
    parts = expression.strip().split()
    if len(parts) == 6:
        # Could be 6-field with seconds (Quartz/Spring) or 6-field Linux
        if re.search(r'[?L#W]', parts[5]):
            return 'quartz'
        return '6field-seconds'
    if len(parts) == 7:
        return 'quartz-7field'
    if len(parts) == 5:
        return 'standard'
    return 'unknown'


def explain_field(value: str, field: str, names: Optional[list] = None) -> str:
    """Convert a single cron field to English."""

    if value == '*':
        return None  # "every" is implied

    if value == '?':
        return None  # Quartz "any" — implied

    # Step: */N or start/N
    step_match = re.match(r'^(\*|\d+)/(\d+)$', value)
    if step_match:
        start = step_match.group(1)
        step = int(step_match.group(2))
        if field == 'minute':
            return f'every {step} minute{"s" if step > 1 else ""}'
        if field == 'hour':
            return f'every {step} hour{"s" if step > 1 else ""}'
        if field == 'day_of_month':
            return f'every {step} day{"s" if step > 1 else ""}'
        if field == 'month':
            return f'every {step} month{"s" if step > 1 else ""}'
        if field == 'second':
            return f'every {step} second{"s" if step > 1 else ""}'
        return f'every {step}'

    # Range: N-M
    range_match = re.match(r'^(\d+)-(\d+)$', value)
    if range_match:
        lo, hi = int(range_match.group(1)), int(range_match.group(2))
        if names:
            lo_name = names[lo] if lo < len(names) else str(lo)
            hi_name = names[hi] if hi < len(names) else str(hi)
            return f'{lo_name} through {hi_name}'
        if field == 'hour':
            lo_fmt = f'{lo}:00 {"AM" if lo < 12 else "PM"}'
            hi_fmt = f'{hi % 12 or 12}:00 {"AM" if hi < 12 else "PM"}'
            return f'between {lo_fmt} and {hi_fmt}'
        return f'{lo} to {hi}'

    # Range with step: N-M/S
    range_step_match = re.match(r'^(\d+)-(\d+)/(\d+)$', value)
    if range_step_match:
        lo = int(range_step_match.group(1))
        hi = int(range_step_match.group(2))
        step = int(range_step_match.group(3))
        range_desc = explain_field(f'{lo}-{hi}', field, names)
        return f'every {step} {field.replace("_", " ")}{"s" if step > 1 else ""} {range_desc}'

    # List: N,M,P
    if ',' in value:
        parts = [p.strip() for p in value.split(',')]
        if names:
            named = [names[int(p)] if p.isdigit() and int(p) < len(names) else p for p in parts]
        else:
            named = parts
        if len(named) == 2:
            return f'{named[0]} and {named[1]}'
        return ', '.join(named[:-1]) + f', and {named[-1]}'

    # Single value
    if value.isdigit():
        n = int(value)
        if field == 'hour':
            if n == 0: return 'midnight'
            if n == 12: return 'noon'
            if n < 12: return f'{n}:00 AM'
            return f'{n - 12}:00 PM'
        if field == 'minute':
            return f'minute {n}' if n != 0 else 'on the hour'
        if field == 'second':
            return f'second {n}'
        if field == 'day_of_month':
            suffix = 'st' if n % 10 == 1 and n != 11 else \
                     'nd' if n % 10 == 2 and n != 12 else \
                     'rd' if n % 10 == 3 and n != 13 else 'th'
            return f'the {n}{suffix}'
        if field == 'month' and names:
            return names[n - 1] if 1 <= n <= 12 else str(n)
        if field == 'day_of_week' and names:
            return names[n % 7]
        return str(n)

    # Special Quartz: L (last), W (nearest weekday), # (Nth weekday)
    if value.upper() == 'L' and field == 'day_of_month':
        return 'the last day of the month'
    if value.u