| import { |
| CRON_CREATE_TOOL_NAME, |
| CRON_DELETE_TOOL_NAME, |
| DEFAULT_MAX_AGE_DAYS, |
| isKairosCronEnabled, |
| } from '../../tools/ScheduleCronTool/prompt.js' |
| import { registerBundledSkill } from '../bundledSkills.js' |
|
|
| const DEFAULT_INTERVAL = '10m' |
|
|
| const USAGE_MESSAGE = `Usage: /loop [interval] <prompt> |
| |
| Run a prompt or slash command on a recurring interval. |
| |
| Intervals: Ns, Nm, Nh, Nd (e.g. 5m, 30m, 2h, 1d). Minimum granularity is 1 minute. |
| If no interval is specified, defaults to ${DEFAULT_INTERVAL}. |
| |
| Examples: |
| /loop 5m /babysit-prs |
| /loop 30m check the deploy |
| /loop 1h /standup 1 |
| /loop check the deploy (defaults to ${DEFAULT_INTERVAL}) |
| /loop check the deploy every 20m` |
|
|
| function buildPrompt(args: string): string { |
| return `# /loop β schedule a recurring prompt |
| |
| Parse the input below into \`[interval] <promptβ¦>\` and schedule it with ${CRON_CREATE_TOOL_NAME}. |
| |
| ## Parsing (in priority order) |
| |
| 1. **Leading token**: if the first whitespace-delimited token matches \`^\\d+[smhd]$\` (e.g. \`5m\`, \`2h\`), that's the interval; the rest is the prompt. |
| 2. **Trailing "every" clause**: otherwise, if the input ends with \`every <N><unit>\` or \`every <N> <unit-word>\` (e.g. \`every 20m\`, \`every 5 minutes\`, \`every 2 hours\`), extract that as the interval and strip it from the prompt. Only match when what follows "every" is a time expression β \`check every PR\` has no interval. |
| 3. **Default**: otherwise, interval is \`${DEFAULT_INTERVAL}\` and the entire input is the prompt. |
| |
| If the resulting prompt is empty, show usage \`/loop [interval] <prompt>\` and stop β do not call ${CRON_CREATE_TOOL_NAME}. |
| |
| Examples: |
| - \`5m /babysit-prs\` β interval \`5m\`, prompt \`/babysit-prs\` (rule 1) |
| - \`check the deploy every 20m\` β interval \`20m\`, prompt \`check the deploy\` (rule 2) |
| - \`run tests every 5 minutes\` β interval \`5m\`, prompt \`run tests\` (rule 2) |
| - \`check the deploy\` β interval \`${DEFAULT_INTERVAL}\`, prompt \`check the deploy\` (rule 3) |
| - \`check every PR\` β interval \`${DEFAULT_INTERVAL}\`, prompt \`check every PR\` (rule 3 β "every" not followed by time) |
| - \`5m\` β empty prompt β show usage |
| |
| ## Interval β cron |
| |
| Supported suffixes: \`s\` (seconds, rounded up to nearest minute, min 1), \`m\` (minutes), \`h\` (hours), \`d\` (days). Convert: |
| |
| | Interval pattern | Cron expression | Notes | |
| |-----------------------|---------------------|------------------------------------------| |
| | \`Nm\` where N β€ 59 | \`*/N * * * *\` | every N minutes | |
| | \`Nm\` where N β₯ 60 | \`0 */H * * *\` | round to hours (H = N/60, must divide 24)| |
| | \`Nh\` where N β€ 23 | \`0 */N * * *\` | every N hours | |
| | \`Nd\` | \`0 0 */N * *\` | every N days at midnight local | |
| | \`Ns\` | treat as \`ceil(N/60)m\` | cron minimum granularity is 1 minute | |
| |
| **If the interval doesn't cleanly divide its unit** (e.g. \`7m\` β \`*/7 * * * *\` gives uneven gaps at :56β:00; \`90m\` β 1.5h which cron can't express), pick the nearest clean interval and tell the user what you rounded to before scheduling. |
| |
| ## Action |
| |
| 1. Call ${CRON_CREATE_TOOL_NAME} with: |
| - \`cron\`: the expression from the table above |
| - \`prompt\`: the parsed prompt from above, verbatim (slash commands are passed through unchanged) |
| - \`recurring\`: \`true\` |
| 2. Briefly confirm: what's scheduled, the cron expression, the human-readable cadence, that recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days, and that they can cancel sooner with ${CRON_DELETE_TOOL_NAME} (include the job ID). |
| 3. **Then immediately execute the parsed prompt now** β don't wait for the first cron fire. If it's a slash command, invoke it via the Skill tool; otherwise act on it directly. |
| |
| ## Input |
| |
| ${args}` |
| } |
|
|
| export function registerLoopSkill(): void { |
| registerBundledSkill({ |
| name: 'loop', |
| description: |
| 'Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo, defaults to 10m)', |
| whenToUse: |
| 'When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. "check the deploy every 5 minutes", "keep running /babysit-prs"). Do NOT invoke for one-off tasks.', |
| argumentHint: '[interval] <prompt>', |
| userInvocable: true, |
| isEnabled: isKairosCronEnabled, |
| async getPromptForCommand(args) { |
| const trimmed = args.trim() |
| if (!trimmed) { |
| return [{ type: 'text', text: USAGE_MESSAGE }] |
| } |
| return [{ type: 'text', text: buildPrompt(trimmed) }] |
| }, |
| }) |
| } |
|
|