source dump of claude code
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 157 lines 5.7 kB view raw
1import { z } from 'zod/v4' 2import { setScheduledTasksEnabled } from '../../bootstrap/state.js' 3import type { ValidationResult } from '../../Tool.js' 4import { buildTool, type ToolDef } from '../../Tool.js' 5import { cronToHuman, parseCronExpression } from '../../utils/cron.js' 6import { 7 addCronTask, 8 getCronFilePath, 9 listAllCronTasks, 10 nextCronRunMs, 11} from '../../utils/cronTasks.js' 12import { lazySchema } from '../../utils/lazySchema.js' 13import { semanticBoolean } from '../../utils/semanticBoolean.js' 14import { getTeammateContext } from '../../utils/teammateContext.js' 15import { 16 buildCronCreateDescription, 17 buildCronCreatePrompt, 18 CRON_CREATE_TOOL_NAME, 19 DEFAULT_MAX_AGE_DAYS, 20 isDurableCronEnabled, 21 isKairosCronEnabled, 22} from './prompt.js' 23import { renderCreateResultMessage, renderCreateToolUseMessage } from './UI.js' 24 25const MAX_JOBS = 50 26 27const inputSchema = lazySchema(() => 28 z.strictObject({ 29 cron: z 30 .string() 31 .describe( 32 'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).', 33 ), 34 prompt: z.string().describe('The prompt to enqueue at each fire time.'), 35 recurring: semanticBoolean(z.boolean().optional()).describe( 36 `true (default) = fire on every cron match until deleted or auto-expired after ${DEFAULT_MAX_AGE_DAYS} days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.`, 37 ), 38 durable: semanticBoolean(z.boolean().optional()).describe( 39 'true = persist to .claude/scheduled_tasks.json and survive restarts. false (default) = in-memory only, dies when this Claude session ends. Use true only when the user asks the task to survive across sessions.', 40 ), 41 }), 42) 43type InputSchema = ReturnType<typeof inputSchema> 44 45const outputSchema = lazySchema(() => 46 z.object({ 47 id: z.string(), 48 humanSchedule: z.string(), 49 recurring: z.boolean(), 50 durable: z.boolean().optional(), 51 }), 52) 53type OutputSchema = ReturnType<typeof outputSchema> 54export type CreateOutput = z.infer<OutputSchema> 55 56export const CronCreateTool = buildTool({ 57 name: CRON_CREATE_TOOL_NAME, 58 searchHint: 'schedule a recurring or one-shot prompt', 59 maxResultSizeChars: 100_000, 60 shouldDefer: true, 61 get inputSchema(): InputSchema { 62 return inputSchema() 63 }, 64 get outputSchema(): OutputSchema { 65 return outputSchema() 66 }, 67 isEnabled() { 68 return isKairosCronEnabled() 69 }, 70 toAutoClassifierInput(input) { 71 return `${input.cron}: ${input.prompt}` 72 }, 73 async description() { 74 return buildCronCreateDescription(isDurableCronEnabled()) 75 }, 76 async prompt() { 77 return buildCronCreatePrompt(isDurableCronEnabled()) 78 }, 79 getPath() { 80 return getCronFilePath() 81 }, 82 async validateInput(input): Promise<ValidationResult> { 83 if (!parseCronExpression(input.cron)) { 84 return { 85 result: false, 86 message: `Invalid cron expression '${input.cron}'. Expected 5 fields: M H DoM Mon DoW.`, 87 errorCode: 1, 88 } 89 } 90 if (nextCronRunMs(input.cron, Date.now()) === null) { 91 return { 92 result: false, 93 message: `Cron expression '${input.cron}' does not match any calendar date in the next year.`, 94 errorCode: 2, 95 } 96 } 97 const tasks = await listAllCronTasks() 98 if (tasks.length >= MAX_JOBS) { 99 return { 100 result: false, 101 message: `Too many scheduled jobs (max ${MAX_JOBS}). Cancel one first.`, 102 errorCode: 3, 103 } 104 } 105 // Teammates don't persist across sessions, so a durable teammate cron 106 // would orphan on restart (agentId would point to a nonexistent teammate). 107 if (input.durable && getTeammateContext()) { 108 return { 109 result: false, 110 message: 111 'durable crons are not supported for teammates (teammates do not persist across sessions)', 112 errorCode: 4, 113 } 114 } 115 return { result: true } 116 }, 117 async call({ cron, prompt, recurring = true, durable = false }) { 118 // Kill switch forces session-only; schema stays stable so the model sees 119 // no validation errors when the gate flips mid-session. 120 const effectiveDurable = durable && isDurableCronEnabled() 121 const id = await addCronTask( 122 cron, 123 prompt, 124 recurring, 125 effectiveDurable, 126 getTeammateContext()?.agentId, 127 ) 128 // Enable the scheduler so the task fires in this session. The 129 // useScheduledTasks hook polls this flag and will start watching 130 // on the next tick. For durable: false tasks the file never changes 131 // — check() reads the session store directly — but the enable flag 132 // is still what starts the tick loop. 133 setScheduledTasksEnabled(true) 134 return { 135 data: { 136 id, 137 humanSchedule: cronToHuman(cron), 138 recurring, 139 durable: effectiveDurable, 140 }, 141 } 142 }, 143 mapToolResultToToolResultBlockParam(output, toolUseID) { 144 const where = output.durable 145 ? 'Persisted to .claude/scheduled_tasks.json' 146 : 'Session-only (not written to disk, dies when Claude exits)' 147 return { 148 tool_use_id: toolUseID, 149 type: 'tool_result', 150 content: output.recurring 151 ? `Scheduled recurring job ${output.id} (${output.humanSchedule}). ${where}. Auto-expires after ${DEFAULT_MAX_AGE_DAYS} days. Use CronDelete to cancel sooner.` 152 : `Scheduled one-shot task ${output.id} (${output.humanSchedule}). ${where}. It will fire once then auto-delete.`, 153 } 154 }, 155 renderToolUseMessage: renderCreateToolUseMessage, 156 renderToolResultMessage: renderCreateResultMessage, 157} satisfies ToolDef<InputSchema, CreateOutput>)