mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
Fix variable name breakage (run_agent, hermes_constants, etc.) where import rewriter changed 'import X' to 'import hermes_agent.Y' but test code still referenced 'X' as a variable name. Fix package-vs-module confusion (cli.auth, cli.models, cli.ui) where single files became directories. Fix hardcoded file paths in tests pointing to old locations. Fix tool registry to discover tools in subpackage directories. Fix stale import in hermes_agent/tools/__init__.py. Part of #14182, #14183
289 lines
10 KiB
Python
289 lines
10 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__).resolve().parents[2].resolve()
|
|
|
|
from hermes_agent.cli.ui.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 hermes_agent.tools.cronjob import cronjob as cronjob_tool
|
|
|
|
return json.loads(cronjob_tool(**kwargs))
|
|
|
|
|
|
def cron_list(show_all: bool = False):
|
|
"""List all scheduled jobs."""
|
|
from hermes_agent.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)}")
|
|
script = job.get("script")
|
|
if script:
|
|
print(f" Script: {script}")
|
|
|
|
# Execution history
|
|
last_status = job.get("last_status")
|
|
if last_status:
|
|
last_run = job.get("last_run_at", "?")
|
|
if last_status == "ok":
|
|
status_display = color("ok", Colors.GREEN)
|
|
else:
|
|
status_display = color(f"{last_status}: {job.get('last_error', '?')}", Colors.RED)
|
|
print(f" Last run: {last_run} {status_display}")
|
|
|
|
delivery_err = job.get("last_delivery_error")
|
|
if delivery_err:
|
|
print(f" {color('⚠ Delivery failed:', Colors.YELLOW)} {delivery_err}")
|
|
|
|
print()
|
|
|
|
from hermes_agent.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 hermes_agent.cron.scheduler import tick
|
|
tick(verbose=True)
|
|
|
|
|
|
def cron_status():
|
|
"""Show cron execution status."""
|
|
from hermes_agent.cron.jobs import list_jobs
|
|
from hermes_agent.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)),
|
|
script=getattr(args, "script", 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'])}")
|
|
job_data = result.get("job", {})
|
|
if job_data.get("script"):
|
|
print(f" Script: {job_data['script']}")
|
|
print(f" Next run: {result['next_run_at']}")
|
|
return 0
|
|
|
|
|
|
def cron_edit(args):
|
|
from hermes_agent.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,
|
|
script=getattr(args, "script", None),
|
|
)
|
|
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")
|
|
if updated.get("script"):
|
|
print(f" Script: {updated['script']}")
|
|
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)
|