feat(cron): Cron Recipes — parameterized automation templates across every surface
A 'recipe' is a one-place definition of an automation that every surface renders natively. The slot schema (cron/recipe_catalog.py) is the single source of truth; four renderers consume it, and all paths end at the same cron.jobs.create_job — no second job engine. Form where there's a screen, conversation where there's a chat line: - Dashboard / GUI app: a Recipes sub-tab on the Cron page renders each recipe's typed slots as a form (time-picker, enum dropdown, free-text); submit POSTs /api/cron/recipes/instantiate which fills + creates the job. - CLI / TUI / messengers: /cron-recipe lists the catalog, shows a recipe's fields, or fills + creates from a pasted 'key slot=val' command. The shared handler (hermes_cli/cron_recipe_cmd.py) names any missing/invalid slot so the agent can ask a targeted follow-up. - Docs: a generated Cron Recipes catalog page (website, .mdx + React cards) shows each recipe with a copy-paste command and a 'Send to App' button. - Desktop: a hermes:// URL scheme (Electron single-instance lock + setAsDefaultProtocolClient + open-url/second-instance) routes hermes://cron-recipe/<key>?slot=val into the chat composer pre-filled. Typed slots (time/enum/text/weekdays) with defaults: users never type raw cron — recipes parameterize time-of-day and weekday sets and translate to cron expressions; a free-text 'schedule' slot is the full-flexibility escape hatch. Consent-first throughout: nothing schedules without an explicit submit or send. Core: - cron/recipe_catalog.py — CronRecipe + RecipeSlot, 5 curated recipes, recipe_form_schema / recipe_slash_command / recipe_deeplink / recipe_catalog_entry renderers, fill_recipe (validate + translate to create_job kwargs). - hermes_cli/cron_recipe_cmd.py — shared /cron-recipe handler (CLI + TUI + gateway never drift). CommandDef + dispatch in commands.py / cli.py / gateway/run.py. Dashboard: GET /api/cron/recipes + POST /api/cron/recipes/instantiate (web_server.py), CronRecipes.tsx gallery+form, Segmented sub-tab on CronPage, api.ts methods + types. Desktop: hermes:// scheme end to end (main.cjs deep-link router + ready-queue, preload onDeepLink/signalDeepLinkReady, global.d.ts types, desktop-controller composer prefill, electron-builder protocols key). Docs: extract-cron-recipes.py generator wired into prebuild.mjs, cron-recipes-catalog.mdx + CronRecipesCatalog React component, sidebar entry. Generated index json gitignored like skills.json. Tests: 23 core (catalog/slots/schedule-resolution/validation/renderers/command handler/generator) + 5 web_server endpoint tests. E2E verified end to end: slot fill -> create_job -> persisted job with correct schedule/deliver/origin.
This commit is contained in:
parent
9a09ea69fb
commit
1593ca5406
25 changed files with 1975 additions and 0 deletions
114
website/src/components/CronRecipesCatalog/styles.module.css
Normal file
114
website/src/components/CronRecipesCatalog/styles.module.css
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 1rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
border-radius: 10px;
|
||||
padding: 1.1rem 1.2rem;
|
||||
background: var(--ifm-card-background-color, var(--ifm-background-surface-color));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.cardHead {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.schedule {
|
||||
font-size: 0.8rem;
|
||||
color: var(--ifm-color-emphasis-700);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.desc {
|
||||
margin: 0;
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
font-size: 0.72rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
}
|
||||
|
||||
.cmdRow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.cmd {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
padding: 0.45rem 0.6rem;
|
||||
font-size: 0.82rem;
|
||||
border-radius: 6px;
|
||||
background: var(--ifm-color-emphasis-100);
|
||||
}
|
||||
|
||||
.copyBtn {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--ifm-color-emphasis-300);
|
||||
background: transparent;
|
||||
color: var(--ifm-color-emphasis-800);
|
||||
border-radius: 6px;
|
||||
padding: 0.35rem 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.copyBtn:hover {
|
||||
background: var(--ifm-color-emphasis-200);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.appBtn {
|
||||
display: inline-block;
|
||||
padding: 0.4rem 0.85rem;
|
||||
border-radius: 6px;
|
||||
background: var(--ifm-color-primary);
|
||||
color: var(--ifm-color-primary-contrast-background, #fff);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.appBtn:hover {
|
||||
background: var(--ifm-color-primary-dark);
|
||||
text-decoration: none;
|
||||
color: var(--ifm-color-primary-contrast-background, #fff);
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 0.78rem;
|
||||
color: var(--ifm-color-emphasis-600);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue