Remove [:8] truncation from hermes cron list output. Job IDs are 12 hex chars — truncating to 8 makes them unusable for cron run/pause/remove which require the full ID. Co-authored-by: vitobotta <vitobotta@users.noreply.github.com>
266 lines
9.3 KiB
Python
266 lines
9.3 KiB
Python
"""
|
|
Cron subcommand for hermes CLI.
|
|
|
|
Handles standalone cron management commands like list, create, edit,
|
|
pause/resume/run/remove, status, and tick.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Iterable, List, Optional
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent.resolve()
|
|
sys.path.insert(0, str(PROJECT_ROOT))
|
|
|
|
from hermes_cli.colors import Colors, color
|
|
|
|
|
|
def _normalize_skills(single_skill=None, skills: Optional[Iterable[str]] = None) -> Optional[List[str]]:
|
|
if skills is None:
|
|
if single_skill is None:
|
|
return None
|
|
raw_items = [single_skill]
|
|
else:
|
|
raw_items = list(skills)
|
|
|
|
normalized: List[str] = []
|
|
for item in raw_items:
|
|
text = str(item or "").strip()
|
|
if text and text not in normalized:
|
|
normalized.append(text)
|
|
return normalized
|
|
|
|
|
|
def _cron_api(**kwargs):
|
|
from tools.cronjob_tools import cronjob as cronjob_tool
|
|
|
|
return json.loads(cronjob_tool(**kwargs))
|
|
|
|
|
|
def cron_list(show_all: bool = False):
|
|
"""List all scheduled jobs."""
|
|
from cron.jobs import list_jobs
|
|
|
|
jobs = list_jobs(include_disabled=show_all)
|
|
|
|
if not jobs:
|
|
print(color("No scheduled jobs.", Colors.DIM))
|
|
print(color("Create one with 'hermes cron create ...' or the /cron command in chat.", Colors.DIM))
|
|
return
|
|
|
|
print()
|
|
print(color("┌─────────────────────────────────────────────────────────────────────────┐", Colors.CYAN))
|
|
print(color("│ Scheduled Jobs │", Colors.CYAN))
|
|
print(color("└─────────────────────────────────────────────────────────────────────────┘", Colors.CYAN))
|
|
print()
|
|
|
|
for job in jobs:
|
|
job_id = job.get("id", "?")
|
|
name = job.get("name", "(unnamed)")
|
|
schedule = job.get("schedule_display", job.get("schedule", {}).get("value", "?"))
|
|
state = job.get("state", "scheduled" if job.get("enabled", True) else "paused")
|
|
next_run = job.get("next_run_at", "?")
|
|
|
|
repeat_info = job.get("repeat", {})
|
|
repeat_times = repeat_info.get("times")
|
|
repeat_completed = repeat_info.get("completed", 0)
|
|
repeat_str = f"{repeat_completed}/{repeat_times}" if repeat_times else "∞"
|
|
|
|
deliver = job.get("deliver", ["local"])
|
|
if isinstance(deliver, str):
|
|
deliver = [deliver]
|
|
deliver_str = ", ".join(deliver)
|
|
|
|
skills = job.get("skills") or ([job["skill"]] if job.get("skill") else [])
|
|
if state == "paused":
|
|
status = color("[paused]", Colors.YELLOW)
|
|
elif state == "completed":
|
|
status = color("[completed]", Colors.BLUE)
|
|
elif job.get("enabled", True):
|
|
status = color("[active]", Colors.GREEN)
|
|
else:
|
|
status = color("[disabled]", Colors.RED)
|
|
|
|
print(f" {color(job_id, Colors.YELLOW)} {status}")
|
|
print(f" Name: {name}")
|
|
print(f" Schedule: {schedule}")
|
|
print(f" Repeat: {repeat_str}")
|
|
print(f" Next run: {next_run}")
|
|
print(f" Deliver: {deliver_str}")
|
|
if skills:
|
|
print(f" Skills: {', '.join(skills)}")
|
|
print()
|
|
|
|
from hermes_cli.gateway import find_gateway_pids
|
|
if not find_gateway_pids():
|
|
print(color(" ⚠ Gateway is not running — jobs won't fire automatically.", Colors.YELLOW))
|
|
print(color(" Start it with: hermes gateway install", Colors.DIM))
|
|
print(color(" sudo hermes gateway install --system # Linux servers", Colors.DIM))
|
|
print()
|
|
|
|
|
|
def cron_tick():
|
|
"""Run due jobs once and exit."""
|
|
from cron.scheduler import tick
|
|
tick(verbose=True)
|
|
|
|
|
|
def cron_status():
|
|
"""Show cron execution status."""
|
|
from cron.jobs import list_jobs
|
|
from hermes_cli.gateway import find_gateway_pids
|
|
|
|
print()
|
|
|
|
pids = find_gateway_pids()
|
|
if pids:
|
|
print(color("✓ Gateway is running — cron jobs will fire automatically", Colors.GREEN))
|
|
print(f" PID: {', '.join(map(str, pids))}")
|
|
else:
|
|
print(color("✗ Gateway is not running — cron jobs will NOT fire", Colors.RED))
|
|
print()
|
|
print(" To enable automatic execution:")
|
|
print(" hermes gateway install # Install as a user service")
|
|
print(" sudo hermes gateway install --system # Linux servers: boot-time system service")
|
|
print(" hermes gateway # Or run in foreground")
|
|
|
|
print()
|
|
|
|
jobs = list_jobs(include_disabled=False)
|
|
if jobs:
|
|
next_runs = [j.get("next_run_at") for j in jobs if j.get("next_run_at")]
|
|
print(f" {len(jobs)} active job(s)")
|
|
if next_runs:
|
|
print(f" Next run: {min(next_runs)}")
|
|
else:
|
|
print(" No active jobs")
|
|
|
|
print()
|
|
|
|
|
|
def cron_create(args):
|
|
result = _cron_api(
|
|
action="create",
|
|
schedule=args.schedule,
|
|
prompt=args.prompt,
|
|
name=getattr(args, "name", None),
|
|
deliver=getattr(args, "deliver", None),
|
|
repeat=getattr(args, "repeat", None),
|
|
skill=getattr(args, "skill", None),
|
|
skills=_normalize_skills(getattr(args, "skill", None), getattr(args, "skills", None)),
|
|
)
|
|
if not result.get("success"):
|
|
print(color(f"Failed to create job: {result.get('error', 'unknown error')}", Colors.RED))
|
|
return 1
|
|
print(color(f"Created job: {result['job_id']}", Colors.GREEN))
|
|
print(f" Name: {result['name']}")
|
|
print(f" Schedule: {result['schedule']}")
|
|
if result.get("skills"):
|
|
print(f" Skills: {', '.join(result['skills'])}")
|
|
print(f" Next run: {result['next_run_at']}")
|
|
return 0
|
|
|
|
|
|
def cron_edit(args):
|
|
from cron.jobs import get_job
|
|
|
|
job = get_job(args.job_id)
|
|
if not job:
|
|
print(color(f"Job not found: {args.job_id}", Colors.RED))
|
|
return 1
|
|
|
|
existing_skills = list(job.get("skills") or ([] if not job.get("skill") else [job.get("skill")]))
|
|
replacement_skills = _normalize_skills(getattr(args, "skill", None), getattr(args, "skills", None))
|
|
add_skills = _normalize_skills(None, getattr(args, "add_skills", None)) or []
|
|
remove_skills = set(_normalize_skills(None, getattr(args, "remove_skills", None)) or [])
|
|
|
|
final_skills = None
|
|
if getattr(args, "clear_skills", False):
|
|
final_skills = []
|
|
elif replacement_skills is not None:
|
|
final_skills = replacement_skills
|
|
elif add_skills or remove_skills:
|
|
final_skills = [skill for skill in existing_skills if skill not in remove_skills]
|
|
for skill in add_skills:
|
|
if skill not in final_skills:
|
|
final_skills.append(skill)
|
|
|
|
result = _cron_api(
|
|
action="update",
|
|
job_id=args.job_id,
|
|
schedule=getattr(args, "schedule", None),
|
|
prompt=getattr(args, "prompt", None),
|
|
name=getattr(args, "name", None),
|
|
deliver=getattr(args, "deliver", None),
|
|
repeat=getattr(args, "repeat", None),
|
|
skills=final_skills,
|
|
)
|
|
if not result.get("success"):
|
|
print(color(f"Failed to update job: {result.get('error', 'unknown error')}", Colors.RED))
|
|
return 1
|
|
|
|
updated = result["job"]
|
|
print(color(f"Updated job: {updated['job_id']}", Colors.GREEN))
|
|
print(f" Name: {updated['name']}")
|
|
print(f" Schedule: {updated['schedule']}")
|
|
if updated.get("skills"):
|
|
print(f" Skills: {', '.join(updated['skills'])}")
|
|
else:
|
|
print(" Skills: none")
|
|
return 0
|
|
|
|
|
|
def _job_action(action: str, job_id: str, success_verb: str) -> int:
|
|
result = _cron_api(action=action, job_id=job_id)
|
|
if not result.get("success"):
|
|
print(color(f"Failed to {action} job: {result.get('error', 'unknown error')}", Colors.RED))
|
|
return 1
|
|
job = result.get("job") or result.get("removed_job") or {}
|
|
print(color(f"{success_verb} job: {job.get('name', job_id)} ({job_id})", Colors.GREEN))
|
|
if action in {"resume", "run"} and result.get("job", {}).get("next_run_at"):
|
|
print(f" Next run: {result['job']['next_run_at']}")
|
|
if action == "run":
|
|
print(" It will run on the next scheduler tick.")
|
|
return 0
|
|
|
|
|
|
def cron_command(args):
|
|
"""Handle cron subcommands."""
|
|
subcmd = getattr(args, 'cron_command', None)
|
|
|
|
if subcmd is None or subcmd == "list":
|
|
show_all = getattr(args, 'all', False)
|
|
cron_list(show_all)
|
|
return 0
|
|
|
|
if subcmd == "status":
|
|
cron_status()
|
|
return 0
|
|
|
|
if subcmd == "tick":
|
|
cron_tick()
|
|
return 0
|
|
|
|
if subcmd in {"create", "add"}:
|
|
return cron_create(args)
|
|
|
|
if subcmd == "edit":
|
|
return cron_edit(args)
|
|
|
|
if subcmd == "pause":
|
|
return _job_action("pause", args.job_id, "Paused")
|
|
|
|
if subcmd == "resume":
|
|
return _job_action("resume", args.job_id, "Resumed")
|
|
|
|
if subcmd == "run":
|
|
return _job_action("run", args.job_id, "Triggered")
|
|
|
|
if subcmd in {"remove", "rm", "delete"}:
|
|
return _job_action("remove", args.job_id, "Removed")
|
|
|
|
print(f"Unknown cron command: {subcmd}")
|
|
print("Usage: hermes cron [list|create|edit|pause|resume|run|remove|status|tick]")
|
|
sys.exit(1)
|