@elizaos/plugin-cron 2.0.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2753 @@
1
+ import {
2
+ DEFAULT_CRON_CONFIG,
3
+ HEARTBEAT_WORKER_NAME,
4
+ drainSystemEvents,
5
+ executeOttoJob,
6
+ heartbeatWorker,
7
+ isOttoPayload,
8
+ isWithinActiveHours,
9
+ normalizeCronJobCreate,
10
+ normalizeCronJobPatch,
11
+ otto_exports,
12
+ pendingEventCount,
13
+ pushSystemEvent,
14
+ readCronRunLogEntries,
15
+ resolveCronRunLogPath,
16
+ resolveCronStorePath,
17
+ resolveHeartbeatConfig,
18
+ startHeartbeat,
19
+ wakeHeartbeatNow
20
+ } from "./chunk-QQWDOGXP.js";
21
+ import "./chunk-MLKGABMK.js";
22
+
23
+ // src/constants.ts
24
+ var CRON_SERVICE_TYPE = "CRON";
25
+ var CRON_JOB_COMPONENT_PREFIX = "cron_job";
26
+ var CRON_JOB_INDEX_COMPONENT = "cron_job_index";
27
+ var CronEvents = {
28
+ CRON_FIRED: "CRON_FIRED",
29
+ CRON_CREATED: "CRON_CREATED",
30
+ CRON_UPDATED: "CRON_UPDATED",
31
+ CRON_DELETED: "CRON_DELETED",
32
+ CRON_FAILED: "CRON_FAILED"
33
+ };
34
+ var CronActions = {
35
+ CREATE_CRON: "CREATE_CRON",
36
+ UPDATE_CRON: "UPDATE_CRON",
37
+ DELETE_CRON: "DELETE_CRON",
38
+ LIST_CRONS: "LIST_CRONS",
39
+ RUN_CRON: "RUN_CRON"
40
+ };
41
+
42
+ // src/scheduler/schedule-utils.ts
43
+ import { Cron } from "croner";
44
+ function parseTimestamp(timestamp) {
45
+ const parsed = Date.parse(timestamp);
46
+ if (Number.isNaN(parsed)) {
47
+ return null;
48
+ }
49
+ return parsed;
50
+ }
51
+ function validateAtSchedule(at) {
52
+ const ms = parseTimestamp(at);
53
+ if (ms === null) {
54
+ return `Invalid timestamp: "${at}". Expected ISO 8601 format (e.g., "2024-12-31T23:59:59Z")`;
55
+ }
56
+ return null;
57
+ }
58
+ function validateEverySchedule(everyMs, config) {
59
+ if (!Number.isFinite(everyMs) || everyMs <= 0) {
60
+ return `Invalid interval: ${everyMs}. Must be a positive number`;
61
+ }
62
+ const minInterval = config.minIntervalMs ?? DEFAULT_CRON_CONFIG.minIntervalMs;
63
+ if (everyMs < minInterval) {
64
+ return `Interval too short: ${everyMs}ms. Minimum allowed is ${minInterval}ms`;
65
+ }
66
+ return null;
67
+ }
68
+ function validateCronExpression(expr, tz) {
69
+ const trimmed = expr.trim();
70
+ if (!trimmed) {
71
+ return "Cron expression cannot be empty";
72
+ }
73
+ const trimmedTz = tz?.trim() || void 0;
74
+ if (trimmedTz) {
75
+ try {
76
+ Intl.DateTimeFormat("en-US", { timeZone: trimmedTz });
77
+ } catch {
78
+ return `Invalid timezone: "${trimmedTz}"`;
79
+ }
80
+ }
81
+ let cron;
82
+ try {
83
+ cron = new Cron(trimmed, { timezone: trimmedTz, catch: false });
84
+ } catch (err) {
85
+ return `Invalid cron expression: ${err instanceof Error ? err.message : String(err)}`;
86
+ }
87
+ const next = cron.nextRun();
88
+ cron.stop();
89
+ if (!next) {
90
+ return `Cron expression "${trimmed}" will never run`;
91
+ }
92
+ return null;
93
+ }
94
+ function validateSchedule(schedule, config) {
95
+ switch (schedule.kind) {
96
+ case "at":
97
+ return validateAtSchedule(schedule.at);
98
+ case "every":
99
+ return validateEverySchedule(schedule.everyMs, config);
100
+ case "cron":
101
+ return validateCronExpression(schedule.expr, schedule.tz);
102
+ default: {
103
+ const _exhaustive = schedule;
104
+ return `Unknown schedule kind: ${_exhaustive.kind}`;
105
+ }
106
+ }
107
+ }
108
+ function computeNextRunAtMs(schedule, nowMs) {
109
+ switch (schedule.kind) {
110
+ case "at": {
111
+ const atMs = parseTimestamp(schedule.at);
112
+ if (atMs === null) {
113
+ return void 0;
114
+ }
115
+ return atMs > nowMs ? atMs : void 0;
116
+ }
117
+ case "every": {
118
+ const everyMs = Math.max(1, Math.floor(schedule.everyMs));
119
+ const anchor = Math.max(0, Math.floor(schedule.anchorMs ?? nowMs));
120
+ if (nowMs < anchor) {
121
+ return anchor;
122
+ }
123
+ const elapsed = nowMs - anchor;
124
+ const steps = Math.max(1, Math.floor((elapsed + everyMs - 1) / everyMs));
125
+ return anchor + steps * everyMs;
126
+ }
127
+ case "cron": {
128
+ const expr = schedule.expr.trim();
129
+ if (!expr) {
130
+ return void 0;
131
+ }
132
+ const cron = new Cron(expr, {
133
+ timezone: schedule.tz?.trim() || void 0,
134
+ catch: false
135
+ });
136
+ const next = cron.nextRun(new Date(nowMs));
137
+ cron.stop();
138
+ return next ? next.getTime() : void 0;
139
+ }
140
+ default: {
141
+ const _exhaustive = schedule;
142
+ return void 0;
143
+ }
144
+ }
145
+ }
146
+ function isJobDue(nextRunAtMs, nowMs, toleranceMs = 1e3) {
147
+ if (nextRunAtMs === void 0) {
148
+ return false;
149
+ }
150
+ return nowMs >= nextRunAtMs - toleranceMs;
151
+ }
152
+ function formatSchedule(schedule) {
153
+ switch (schedule.kind) {
154
+ case "at": {
155
+ const date = new Date(schedule.at);
156
+ return `once at ${date.toLocaleString()}`;
157
+ }
158
+ case "every": {
159
+ const ms = schedule.everyMs;
160
+ if (ms >= 864e5) {
161
+ const days = Math.round(ms / 864e5);
162
+ return `every ${days} day${days === 1 ? "" : "s"}`;
163
+ }
164
+ if (ms >= 36e5) {
165
+ const hours = Math.round(ms / 36e5);
166
+ return `every ${hours} hour${hours === 1 ? "" : "s"}`;
167
+ }
168
+ if (ms >= 6e4) {
169
+ const minutes = Math.round(ms / 6e4);
170
+ return `every ${minutes} minute${minutes === 1 ? "" : "s"}`;
171
+ }
172
+ const seconds = Math.round(ms / 1e3);
173
+ return `every ${seconds} second${seconds === 1 ? "" : "s"}`;
174
+ }
175
+ case "cron": {
176
+ const tz = schedule.tz ? ` (${schedule.tz})` : "";
177
+ return `cron: ${schedule.expr}${tz}`;
178
+ }
179
+ default: {
180
+ const _exhaustive = schedule;
181
+ return "unknown schedule";
182
+ }
183
+ }
184
+ }
185
+ function parseDuration(duration) {
186
+ const match = /^(\d+(?:\.\d+)?)\s*(s|sec|seconds?|m|min|minutes?|h|hr|hours?|d|days?)$/i.exec(
187
+ duration.trim()
188
+ );
189
+ if (!match) {
190
+ return null;
191
+ }
192
+ const value = parseFloat(match[1]);
193
+ if (value <= 0 || !Number.isFinite(value)) {
194
+ return null;
195
+ }
196
+ const unit = match[2].toLowerCase();
197
+ let ms;
198
+ if (unit.startsWith("s")) {
199
+ ms = Math.round(value * 1e3);
200
+ } else if (unit.startsWith("m")) {
201
+ ms = Math.round(value * 6e4);
202
+ } else if (unit.startsWith("h")) {
203
+ ms = Math.round(value * 36e5);
204
+ } else if (unit.startsWith("d")) {
205
+ ms = Math.round(value * 864e5);
206
+ } else {
207
+ return null;
208
+ }
209
+ return ms > 0 ? ms : null;
210
+ }
211
+ function parseScheduleDescription(description, nowMs = Date.now()) {
212
+ const normalized = description.trim().toLowerCase();
213
+ const inMatch = /^in\s+(.+)$/i.exec(normalized);
214
+ if (inMatch) {
215
+ const durationMs = parseDuration(inMatch[1]);
216
+ if (durationMs !== null) {
217
+ return {
218
+ kind: "at",
219
+ at: new Date(nowMs + durationMs).toISOString()
220
+ };
221
+ }
222
+ }
223
+ const everyMatch = /^every\s+(.+)$/i.exec(normalized);
224
+ if (everyMatch) {
225
+ const durationMs = parseDuration(everyMatch[1]);
226
+ if (durationMs !== null) {
227
+ return {
228
+ kind: "every",
229
+ everyMs: durationMs,
230
+ anchorMs: nowMs
231
+ };
232
+ }
233
+ }
234
+ const fields = normalized.split(/\s+/);
235
+ if (fields.length >= 5 && fields.length <= 6) {
236
+ const validation = validateCronExpression(normalized);
237
+ if (validation === null) {
238
+ return {
239
+ kind: "cron",
240
+ expr: normalized
241
+ };
242
+ }
243
+ }
244
+ const timestamp = parseTimestamp(description);
245
+ if (timestamp !== null) {
246
+ return {
247
+ kind: "at",
248
+ at: description
249
+ };
250
+ }
251
+ return void 0;
252
+ }
253
+
254
+ // src/actions/create-cron.ts
255
+ function parseNaturalLanguageRequest(text) {
256
+ const result = {};
257
+ const normalized = text.toLowerCase();
258
+ const everyMatch = /every\s+(\d+\s*(?:second|minute|hour|day|week)s?)/i.exec(text);
259
+ if (everyMatch) {
260
+ result.schedule = parseScheduleDescription(`every ${everyMatch[1]}`);
261
+ }
262
+ const atMatch = /at\s+(\d{1,2}(?::\d{2})?\s*(?:am|pm)?)/i.exec(text);
263
+ if (atMatch && !result.schedule) {
264
+ const timeStr = atMatch[1].toLowerCase();
265
+ let hours;
266
+ let minutes = 0;
267
+ const timeParts = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i.exec(timeStr);
268
+ if (timeParts) {
269
+ hours = parseInt(timeParts[1], 10);
270
+ if (timeParts[2]) {
271
+ minutes = parseInt(timeParts[2], 10);
272
+ }
273
+ if (timeParts[3]) {
274
+ if (timeParts[3].toLowerCase() === "pm" && hours !== 12) {
275
+ hours += 12;
276
+ } else if (timeParts[3].toLowerCase() === "am" && hours === 12) {
277
+ hours = 0;
278
+ }
279
+ }
280
+ if (normalized.includes("daily") || normalized.includes("every day")) {
281
+ result.schedule = {
282
+ kind: "cron",
283
+ expr: `${minutes} ${hours} * * *`
284
+ };
285
+ } else if (normalized.includes("weekday") || normalized.includes("monday to friday")) {
286
+ result.schedule = {
287
+ kind: "cron",
288
+ expr: `${minutes} ${hours} * * 1-5`
289
+ };
290
+ } else if (normalized.includes("weekend")) {
291
+ result.schedule = {
292
+ kind: "cron",
293
+ expr: `${minutes} ${hours} * * 0,6`
294
+ };
295
+ } else {
296
+ result.schedule = {
297
+ kind: "cron",
298
+ expr: `${minutes} ${hours} * * *`
299
+ };
300
+ }
301
+ }
302
+ }
303
+ const inMatch = /in\s+(\d+\s*(?:second|minute|hour|day)s?)/i.exec(text);
304
+ if (inMatch && !result.schedule) {
305
+ result.schedule = parseScheduleDescription(`in ${inMatch[1]}`);
306
+ }
307
+ const toMatch = /(?:to|that)\s+(.+?)(?:\s+every|\s+at|\s+in\s+\d|$)/i.exec(text);
308
+ if (toMatch) {
309
+ result.prompt = toMatch[1].trim();
310
+ result.name = toMatch[1].slice(0, 50).trim();
311
+ }
312
+ const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
313
+ if (nameMatch) {
314
+ result.name = nameMatch[1].trim();
315
+ }
316
+ if (!result.name && result.schedule) {
317
+ const scheduleDesc = formatSchedule(result.schedule);
318
+ result.name = `Cron job (${scheduleDesc})`;
319
+ }
320
+ return result;
321
+ }
322
+ function formatJobResponse(job) {
323
+ const scheduleStr = formatSchedule(job.schedule);
324
+ const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
325
+ return `Created cron job "${job.name}"
326
+ - ID: ${job.id}
327
+ - Schedule: ${scheduleStr}
328
+ - Status: ${job.enabled ? "enabled" : "disabled"}
329
+ - Next run: ${nextRun}`;
330
+ }
331
+ var createCronAction = {
332
+ name: CronActions.CREATE_CRON,
333
+ similes: [
334
+ "SCHEDULE_CRON",
335
+ "ADD_CRON",
336
+ "NEW_CRON",
337
+ "CREATE_SCHEDULED_JOB",
338
+ "SET_UP_CRON",
339
+ "SCHEDULE_JOB",
340
+ "CREATE_RECURRING_JOB"
341
+ ],
342
+ description: "Creates a new cron job that runs on a schedule. Supports interval-based schedules (every X minutes), cron expressions, and one-time schedules.",
343
+ validate: async (_runtime, message) => {
344
+ const text = message.content?.text?.toLowerCase() ?? "";
345
+ const hasScheduleKeyword = text.includes("cron") || text.includes("schedule") || text.includes("every ") || text.includes("recurring") || text.includes("repeat") || text.includes("daily") || text.includes("hourly") || text.includes("weekly");
346
+ const hasCreateIntent = text.includes("create") || text.includes("add") || text.includes("set up") || text.includes("schedule") || text.includes("make");
347
+ return hasScheduleKeyword && hasCreateIntent;
348
+ },
349
+ handler: async (runtime, message, _state, options, callback) => {
350
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
351
+ if (!cronService) {
352
+ await callback?.({
353
+ text: "Cron service is not available. Please ensure the plugin is loaded."
354
+ });
355
+ return { success: false, error: "Cron service not available" };
356
+ }
357
+ const text = message.content?.text ?? "";
358
+ if (options?.jobInput && typeof options.jobInput === "object") {
359
+ const input = options.jobInput;
360
+ const scheduleError = validateSchedule(input.schedule, DEFAULT_CRON_CONFIG);
361
+ if (scheduleError) {
362
+ await callback?.({
363
+ text: `Invalid schedule: ${scheduleError}`
364
+ });
365
+ return { success: false, error: scheduleError };
366
+ }
367
+ const job2 = await cronService.createJob(input);
368
+ await callback?.({
369
+ text: formatJobResponse(job2)
370
+ });
371
+ return { success: true, data: { jobId: job2.id, job: job2 } };
372
+ }
373
+ const parsed = parseNaturalLanguageRequest(text);
374
+ if (!parsed.schedule) {
375
+ await callback?.({
376
+ text: `I couldn't understand the schedule. Please specify when the job should run, for example:
377
+ - "every 5 minutes"
378
+ - "every hour"
379
+ - "daily at 9am"
380
+ - "every weekday at 8:30am"`
381
+ });
382
+ return { success: false, error: "Could not parse schedule" };
383
+ }
384
+ const jobInput = {
385
+ name: parsed.name || "Unnamed cron job",
386
+ description: parsed.description,
387
+ enabled: true,
388
+ schedule: parsed.schedule,
389
+ payload: {
390
+ kind: "prompt",
391
+ text: parsed.prompt || "Run scheduled task"
392
+ }
393
+ };
394
+ if (parsed.schedule.kind === "at") {
395
+ jobInput.deleteAfterRun = true;
396
+ }
397
+ const job = await cronService.createJob(jobInput);
398
+ await callback?.({
399
+ text: formatJobResponse(job)
400
+ });
401
+ return {
402
+ success: true,
403
+ data: {
404
+ jobId: job.id,
405
+ job
406
+ }
407
+ };
408
+ },
409
+ examples: [
410
+ [
411
+ {
412
+ name: "{{user1}}",
413
+ content: { text: "Create a cron job to check the news every hour" }
414
+ },
415
+ {
416
+ name: "{{agentName}}",
417
+ content: {
418
+ text: 'Created cron job "check the news"\n- ID: abc-123\n- Schedule: every 1 hour\n- Status: enabled\n- Next run: in 1 hour'
419
+ }
420
+ }
421
+ ],
422
+ [
423
+ {
424
+ name: "{{user1}}",
425
+ content: { text: "Schedule a daily reminder at 9am to review my goals" }
426
+ },
427
+ {
428
+ name: "{{agentName}}",
429
+ content: {
430
+ text: 'Created cron job "review my goals"\n- ID: def-456\n- Schedule: cron: 0 9 * * *\n- Status: enabled\n- Next run: tomorrow at 9:00 AM'
431
+ }
432
+ }
433
+ ],
434
+ [
435
+ {
436
+ name: "{{user1}}",
437
+ content: { text: "Set up a recurring job every 5 minutes to check server status" }
438
+ },
439
+ {
440
+ name: "{{agentName}}",
441
+ content: {
442
+ text: 'Created cron job "check server status"\n- ID: ghi-789\n- Schedule: every 5 minutes\n- Status: enabled\n- Next run: in 5 minutes'
443
+ }
444
+ }
445
+ ]
446
+ ]
447
+ };
448
+
449
+ // src/actions/delete-cron.ts
450
+ function extractJobIdentifier(text) {
451
+ const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(text);
452
+ if (idMatch) {
453
+ return { id: idMatch[1] };
454
+ }
455
+ const quotedMatch = /["']([^"']+)["']/i.exec(text);
456
+ if (quotedMatch) {
457
+ return { name: quotedMatch[1] };
458
+ }
459
+ const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
460
+ if (namedMatch) {
461
+ return { name: namedMatch[1] };
462
+ }
463
+ return {};
464
+ }
465
+ var deleteCronAction = {
466
+ name: CronActions.DELETE_CRON,
467
+ similes: [
468
+ "REMOVE_CRON",
469
+ "CANCEL_CRON",
470
+ "STOP_CRON",
471
+ "DELETE_SCHEDULED_JOB",
472
+ "REMOVE_SCHEDULED_JOB"
473
+ ],
474
+ description: "Deletes a cron job by ID or name, removing it from the schedule permanently.",
475
+ validate: async (_runtime, message) => {
476
+ const text = message.content?.text?.toLowerCase() ?? "";
477
+ const hasDeleteKeyword = text.includes("delete") || text.includes("remove") || text.includes("cancel") || text.includes("stop") && !text.includes("stop running");
478
+ const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
479
+ return hasDeleteKeyword && hasCronKeyword;
480
+ },
481
+ handler: async (runtime, message, _state, options, callback) => {
482
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
483
+ if (!cronService) {
484
+ await callback?.({
485
+ text: "Cron service is not available. Please ensure the plugin is loaded."
486
+ });
487
+ return { success: false, error: "Cron service not available" };
488
+ }
489
+ const text = message.content?.text ?? "";
490
+ let jobId = options?.jobId;
491
+ let jobName;
492
+ if (!jobId) {
493
+ const identifier = extractJobIdentifier(text);
494
+ if (identifier.id) {
495
+ jobId = identifier.id;
496
+ } else if (identifier.name) {
497
+ const jobs = await cronService.listJobs({ includeDisabled: true });
498
+ const job = jobs.find((j) => j.name.toLowerCase() === identifier.name?.toLowerCase());
499
+ if (!job) {
500
+ await callback?.({
501
+ text: `No cron job found with name: ${identifier.name}`
502
+ });
503
+ return { success: false, error: "Job not found" };
504
+ }
505
+ jobId = job.id;
506
+ jobName = job.name;
507
+ }
508
+ }
509
+ if (!jobId) {
510
+ await callback?.({
511
+ text: 'Please specify which cron job to delete. You can use the job ID or name.\nExample: "delete cron job abc-123" or "remove cron called daily-check"'
512
+ });
513
+ return { success: false, error: "No job identifier provided" };
514
+ }
515
+ if (!jobName) {
516
+ const job = await cronService.getJob(jobId);
517
+ if (job) {
518
+ jobName = job.name;
519
+ }
520
+ }
521
+ const deleted = await cronService.deleteJob(jobId);
522
+ if (!deleted) {
523
+ await callback?.({
524
+ text: `No cron job found with ID: ${jobId}`
525
+ });
526
+ return { success: false, error: "Job not found" };
527
+ }
528
+ await callback?.({
529
+ text: `Deleted cron job "${jobName || "unknown"}" (${jobId}).
530
+ The job has been permanently removed and will no longer run.`
531
+ });
532
+ return {
533
+ success: true,
534
+ data: {
535
+ jobId,
536
+ jobName,
537
+ deleted: true
538
+ }
539
+ };
540
+ },
541
+ examples: [
542
+ [
543
+ {
544
+ name: "{{user1}}",
545
+ content: { text: "Delete the cron job called daily-check" }
546
+ },
547
+ {
548
+ name: "{{agentName}}",
549
+ content: {
550
+ text: 'Deleted cron job "daily-check" (abc-123).\nThe job has been permanently removed and will no longer run.'
551
+ }
552
+ }
553
+ ],
554
+ [
555
+ {
556
+ name: "{{user1}}",
557
+ content: { text: "Remove cron abc-123-def-456" }
558
+ },
559
+ {
560
+ name: "{{agentName}}",
561
+ content: {
562
+ text: 'Deleted cron job "hourly-status" (abc-123-def-456).\nThe job has been permanently removed and will no longer run.'
563
+ }
564
+ }
565
+ ]
566
+ ]
567
+ };
568
+
569
+ // src/actions/list-crons.ts
570
+ function formatJobList(jobs, includeDisabled) {
571
+ if (jobs.length === 0) {
572
+ return includeDisabled ? "No cron jobs found." : 'No active cron jobs found. Use "list all crons" to include disabled jobs.';
573
+ }
574
+ const lines = [`Found ${jobs.length} cron job${jobs.length === 1 ? "" : "s"}:
575
+ `];
576
+ for (const job of jobs) {
577
+ const scheduleStr = formatSchedule(job.schedule);
578
+ const statusStr = job.enabled ? "enabled" : "disabled";
579
+ const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
580
+ const lastStatus = job.state.lastStatus ? ` (last: ${job.state.lastStatus})` : "";
581
+ lines.push(
582
+ `\u2022 ${job.name}${lastStatus}
583
+ ID: ${job.id}
584
+ Schedule: ${scheduleStr}
585
+ Status: ${statusStr}
586
+ Next run: ${nextRun}
587
+ Runs: ${job.state.runCount} | Errors: ${job.state.errorCount}`
588
+ );
589
+ }
590
+ return lines.join("\n");
591
+ }
592
+ function formatJobDetails(job) {
593
+ const scheduleStr = formatSchedule(job.schedule);
594
+ const statusStr = job.enabled ? "enabled" : "disabled";
595
+ const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toLocaleString() : "not scheduled";
596
+ const lastRun = job.state.lastRunAtMs ? new Date(job.state.lastRunAtMs).toLocaleString() : "never";
597
+ let details = `Cron Job: ${job.name}
598
+
599
+ `;
600
+ details += `ID: ${job.id}
601
+ `;
602
+ if (job.description) {
603
+ details += `Description: ${job.description}
604
+ `;
605
+ }
606
+ details += `
607
+ Schedule: ${scheduleStr}
608
+ `;
609
+ details += `Status: ${statusStr}
610
+ `;
611
+ if (job.deleteAfterRun) {
612
+ details += `Type: one-shot (will be deleted after successful run)
613
+ `;
614
+ }
615
+ details += `
616
+ Execution Stats:
617
+ `;
618
+ details += ` Next run: ${nextRun}
619
+ `;
620
+ details += ` Last run: ${lastRun}
621
+ `;
622
+ details += ` Total runs: ${job.state.runCount}
623
+ `;
624
+ details += ` Total errors: ${job.state.errorCount}
625
+ `;
626
+ if (job.state.lastStatus) {
627
+ details += ` Last status: ${job.state.lastStatus}
628
+ `;
629
+ }
630
+ if (job.state.lastError) {
631
+ details += ` Last error: ${job.state.lastError}
632
+ `;
633
+ }
634
+ if (job.state.lastDurationMs !== void 0) {
635
+ details += ` Last duration: ${job.state.lastDurationMs}ms
636
+ `;
637
+ }
638
+ details += `
639
+ Payload Type: ${job.payload.kind}
640
+ `;
641
+ if (job.payload.kind === "prompt") {
642
+ details += `Prompt: ${job.payload.text.slice(0, 200)}${job.payload.text.length > 200 ? "..." : ""}
643
+ `;
644
+ } else if (job.payload.kind === "action") {
645
+ details += `Action: ${job.payload.actionName}
646
+ `;
647
+ } else if (job.payload.kind === "event") {
648
+ details += `Event: ${job.payload.eventName}
649
+ `;
650
+ }
651
+ if (job.tags && job.tags.length > 0) {
652
+ details += `
653
+ Tags: ${job.tags.join(", ")}
654
+ `;
655
+ }
656
+ details += `
657
+ Created: ${new Date(job.createdAtMs).toLocaleString()}
658
+ `;
659
+ details += `Updated: ${new Date(job.updatedAtMs).toLocaleString()}
660
+ `;
661
+ return details;
662
+ }
663
+ var listCronsAction = {
664
+ name: CronActions.LIST_CRONS,
665
+ similes: [
666
+ "SHOW_CRONS",
667
+ "GET_CRONS",
668
+ "VIEW_CRONS",
669
+ "LIST_SCHEDULED_JOBS",
670
+ "SHOW_SCHEDULED_JOBS",
671
+ "MY_CRONS",
672
+ "CRON_STATUS"
673
+ ],
674
+ description: "Lists all cron jobs. Can filter by enabled status or show details of a specific job.",
675
+ validate: async (_runtime, message) => {
676
+ const text = message.content?.text?.toLowerCase() ?? "";
677
+ const hasListKeyword = text.includes("list") || text.includes("show") || text.includes("view") || text.includes("get") || text.includes("what");
678
+ const hasCronKeyword = text.includes("cron") || text.includes("scheduled") || text.includes("job") || text.includes("schedule");
679
+ return hasListKeyword && hasCronKeyword;
680
+ },
681
+ handler: async (runtime, message, _state, options, callback) => {
682
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
683
+ if (!cronService) {
684
+ await callback?.({
685
+ text: "Cron service is not available. Please ensure the plugin is loaded."
686
+ });
687
+ return { success: false, error: "Cron service not available" };
688
+ }
689
+ const text = message.content?.text?.toLowerCase() ?? "";
690
+ const idMatch = /(?:job|cron)\s+([a-f0-9-]{36})/i.exec(text);
691
+ if (idMatch) {
692
+ const jobId = idMatch[1];
693
+ const job = await cronService.getJob(jobId);
694
+ if (!job) {
695
+ await callback?.({
696
+ text: `No cron job found with ID: ${jobId}`
697
+ });
698
+ return { success: false, error: "Job not found" };
699
+ }
700
+ await callback?.({
701
+ text: formatJobDetails(job)
702
+ });
703
+ return { success: true, data: { job } };
704
+ }
705
+ const nameMatch = /(?:called|named)\s+["']?([^"']+)["']?/i.exec(text);
706
+ if (nameMatch) {
707
+ const jobName = nameMatch[1].toLowerCase();
708
+ const jobs2 = await cronService.listJobs({ includeDisabled: true });
709
+ const job = jobs2.find((j) => j.name.toLowerCase().includes(jobName));
710
+ if (!job) {
711
+ await callback?.({
712
+ text: `No cron job found with name containing: ${jobName}`
713
+ });
714
+ return { success: false, error: "Job not found" };
715
+ }
716
+ await callback?.({
717
+ text: formatJobDetails(job)
718
+ });
719
+ return { success: true, data: { job } };
720
+ }
721
+ const filter = {};
722
+ if (text.includes("all")) {
723
+ filter.includeDisabled = true;
724
+ }
725
+ if (text.includes("enabled") || text.includes("active")) {
726
+ filter.enabled = true;
727
+ filter.includeDisabled = false;
728
+ }
729
+ if (text.includes("disabled") || text.includes("inactive")) {
730
+ filter.enabled = false;
731
+ filter.includeDisabled = true;
732
+ }
733
+ if (options?.filter && typeof options.filter === "object") {
734
+ Object.assign(filter, options.filter);
735
+ }
736
+ const jobs = await cronService.listJobs(filter);
737
+ await callback?.({
738
+ text: formatJobList(jobs, filter.includeDisabled ?? false)
739
+ });
740
+ return {
741
+ success: true,
742
+ data: {
743
+ jobs,
744
+ count: jobs.length
745
+ }
746
+ };
747
+ },
748
+ examples: [
749
+ [
750
+ {
751
+ name: "{{user1}}",
752
+ content: { text: "List my cron jobs" }
753
+ },
754
+ {
755
+ name: "{{agentName}}",
756
+ content: {
757
+ text: "Found 2 cron jobs:\n\n\u2022 Daily news check\n ID: abc-123\n Schedule: cron: 0 9 * * *\n Status: enabled\n Next run: tomorrow at 9:00 AM\n Runs: 5 | Errors: 0\n\n\u2022 Hourly status check\n ID: def-456\n Schedule: every 1 hour\n Status: enabled\n Next run: in 45 minutes\n Runs: 120 | Errors: 2"
758
+ }
759
+ }
760
+ ],
761
+ [
762
+ {
763
+ name: "{{user1}}",
764
+ content: { text: "Show all crons including disabled" }
765
+ },
766
+ {
767
+ name: "{{agentName}}",
768
+ content: {
769
+ text: "Found 3 cron jobs:\n\n\u2022 Daily news check\n ID: abc-123\n Schedule: cron: 0 9 * * *\n Status: enabled\n ...\n\n\u2022 Old backup job\n ID: xyz-789\n Schedule: every 1 day\n Status: disabled\n ..."
770
+ }
771
+ }
772
+ ]
773
+ ]
774
+ };
775
+
776
+ // src/actions/run-cron.ts
777
+ function extractJobIdentifier2(text) {
778
+ const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(text);
779
+ if (idMatch) {
780
+ return { id: idMatch[1] };
781
+ }
782
+ const quotedMatch = /["']([^"']+)["']/i.exec(text);
783
+ if (quotedMatch) {
784
+ return { name: quotedMatch[1] };
785
+ }
786
+ const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
787
+ if (namedMatch) {
788
+ return { name: namedMatch[1] };
789
+ }
790
+ return {};
791
+ }
792
+ var runCronAction = {
793
+ name: CronActions.RUN_CRON,
794
+ similes: [
795
+ "EXECUTE_CRON",
796
+ "TRIGGER_CRON",
797
+ "FIRE_CRON",
798
+ "RUN_SCHEDULED_JOB",
799
+ "EXECUTE_JOB",
800
+ "TRIGGER_JOB"
801
+ ],
802
+ description: "Manually runs a cron job immediately, regardless of its schedule. Useful for testing or one-off execution.",
803
+ validate: async (_runtime, message) => {
804
+ const text = message.content?.text?.toLowerCase() ?? "";
805
+ const hasRunKeyword = text.includes("run") || text.includes("execute") || text.includes("trigger") || text.includes("fire");
806
+ const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
807
+ const isCreateIntent = text.includes("run every") || text.includes("runs every");
808
+ return hasRunKeyword && hasCronKeyword && !isCreateIntent;
809
+ },
810
+ handler: async (runtime, message, _state, options, callback) => {
811
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
812
+ if (!cronService) {
813
+ await callback?.({
814
+ text: "Cron service is not available. Please ensure the plugin is loaded."
815
+ });
816
+ return { success: false, error: "Cron service not available" };
817
+ }
818
+ const text = message.content?.text ?? "";
819
+ let jobId = options?.jobId;
820
+ let jobName;
821
+ if (!jobId) {
822
+ const identifier = extractJobIdentifier2(text);
823
+ if (identifier.id) {
824
+ jobId = identifier.id;
825
+ } else if (identifier.name) {
826
+ const jobs = await cronService.listJobs({ includeDisabled: true });
827
+ const job = jobs.find((j) => j.name.toLowerCase() === identifier.name?.toLowerCase());
828
+ if (!job) {
829
+ await callback?.({
830
+ text: `No cron job found with name: ${identifier.name}`
831
+ });
832
+ return { success: false, error: "Job not found" };
833
+ }
834
+ jobId = job.id;
835
+ jobName = job.name;
836
+ }
837
+ }
838
+ if (!jobId) {
839
+ await callback?.({
840
+ text: 'Please specify which cron job to run. You can use the job ID or name.\nExample: "run cron job abc-123" or "execute cron called daily-check"'
841
+ });
842
+ return { success: false, error: "No job identifier provided" };
843
+ }
844
+ if (!jobName) {
845
+ const job = await cronService.getJob(jobId);
846
+ if (job) {
847
+ jobName = job.name;
848
+ }
849
+ }
850
+ const result = await cronService.runJob(jobId, "force");
851
+ if (!result.ran) {
852
+ await callback?.({
853
+ text: `Could not run job: ${result.error}`
854
+ });
855
+ return { success: false, error: result.error };
856
+ }
857
+ let responseText = `Ran cron job "${jobName || "unknown"}" (${jobId})
858
+ `;
859
+ responseText += `Status: ${result.status}
860
+ `;
861
+ responseText += `Duration: ${result.durationMs}ms
862
+ `;
863
+ if (result.status === "ok" && result.output) {
864
+ const outputPreview = result.output.length > 500 ? `${result.output.slice(0, 500)}... (truncated)` : result.output;
865
+ responseText += `
866
+ Output:
867
+ ${outputPreview}`;
868
+ }
869
+ if (result.error) {
870
+ responseText += `
871
+ Error: ${result.error}`;
872
+ }
873
+ await callback?.({
874
+ text: responseText
875
+ });
876
+ return {
877
+ success: result.status === "ok",
878
+ data: {
879
+ jobId,
880
+ jobName,
881
+ result
882
+ }
883
+ };
884
+ },
885
+ examples: [
886
+ [
887
+ {
888
+ name: "{{user1}}",
889
+ content: { text: "Run the cron job called daily-check now" }
890
+ },
891
+ {
892
+ name: "{{agentName}}",
893
+ content: {
894
+ text: 'Ran cron job "daily-check" (abc-123)\nStatus: ok\nDuration: 1250ms\n\nOutput:\nDaily check completed successfully. All systems operational.'
895
+ }
896
+ }
897
+ ],
898
+ [
899
+ {
900
+ name: "{{user1}}",
901
+ content: { text: "Execute cron abc-123-def-456" }
902
+ },
903
+ {
904
+ name: "{{agentName}}",
905
+ content: {
906
+ text: 'Ran cron job "status-checker" (abc-123-def-456)\nStatus: ok\nDuration: 850ms'
907
+ }
908
+ }
909
+ ]
910
+ ]
911
+ };
912
+
913
+ // src/actions/update-cron.ts
914
+ function extractJobIdentifier3(text) {
915
+ const idMatch = /([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})/i.exec(text);
916
+ if (idMatch) {
917
+ return { id: idMatch[1] };
918
+ }
919
+ const quotedMatch = /["']([^"']+)["']/i.exec(text);
920
+ if (quotedMatch) {
921
+ return { name: quotedMatch[1] };
922
+ }
923
+ const namedMatch = /(?:job|cron)\s+(?:called|named)\s+(\S+)/i.exec(text);
924
+ if (namedMatch) {
925
+ return { name: namedMatch[1] };
926
+ }
927
+ return {};
928
+ }
929
+ function parseUpdateIntent(text) {
930
+ const patch = {};
931
+ const normalized = text.toLowerCase();
932
+ if (normalized.includes("enable") && !normalized.includes("disable")) {
933
+ patch.enabled = true;
934
+ } else if (normalized.includes("disable")) {
935
+ patch.enabled = false;
936
+ }
937
+ const everyMatch = /every\s+(\d+\s*(?:second|minute|hour|day|week)s?)/i.exec(text);
938
+ if (everyMatch) {
939
+ const schedule = parseScheduleDescription(`every ${everyMatch[1]}`);
940
+ if (schedule) {
941
+ patch.schedule = schedule;
942
+ }
943
+ }
944
+ const renameMatch = /rename\s+(?:to|as)\s+["']?([^"']+)["']?/i.exec(text);
945
+ if (renameMatch) {
946
+ patch.name = renameMatch[1].trim();
947
+ }
948
+ return patch;
949
+ }
950
+ var updateCronAction = {
951
+ name: CronActions.UPDATE_CRON,
952
+ similes: [
953
+ "MODIFY_CRON",
954
+ "EDIT_CRON",
955
+ "CHANGE_CRON",
956
+ "ENABLE_CRON",
957
+ "DISABLE_CRON",
958
+ "PAUSE_CRON",
959
+ "RESUME_CRON"
960
+ ],
961
+ description: "Updates an existing cron job. Can enable/disable jobs, change schedules, or modify other properties.",
962
+ validate: async (_runtime, message) => {
963
+ const text = message.content?.text?.toLowerCase() ?? "";
964
+ const hasUpdateKeyword = text.includes("update") || text.includes("modify") || text.includes("edit") || text.includes("change") || text.includes("enable") || text.includes("disable") || text.includes("pause") || text.includes("resume");
965
+ const hasCronKeyword = text.includes("cron") || text.includes("job") || text.includes("schedule");
966
+ return hasUpdateKeyword && hasCronKeyword;
967
+ },
968
+ handler: async (runtime, message, _state, options, callback) => {
969
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
970
+ if (!cronService) {
971
+ await callback?.({
972
+ text: "Cron service is not available. Please ensure the plugin is loaded."
973
+ });
974
+ return { success: false, error: "Cron service not available" };
975
+ }
976
+ const text = message.content?.text ?? "";
977
+ let jobId = options?.jobId;
978
+ let patch = options?.patch || {};
979
+ if (!jobId) {
980
+ const identifier = extractJobIdentifier3(text);
981
+ if (identifier.id) {
982
+ jobId = identifier.id;
983
+ } else if (identifier.name) {
984
+ const jobs = await cronService.listJobs({ includeDisabled: true });
985
+ const job = jobs.find((j) => j.name.toLowerCase() === identifier.name?.toLowerCase());
986
+ if (!job) {
987
+ await callback?.({
988
+ text: `No cron job found with name: ${identifier.name}`
989
+ });
990
+ return { success: false, error: "Job not found" };
991
+ }
992
+ jobId = job.id;
993
+ }
994
+ }
995
+ if (!jobId) {
996
+ await callback?.({
997
+ text: 'Please specify which cron job to update. You can use the job ID or name.\nExample: "disable cron job abc-123" or "enable cron called daily-check"'
998
+ });
999
+ return { success: false, error: "No job identifier provided" };
1000
+ }
1001
+ if (Object.keys(patch).length === 0) {
1002
+ patch = parseUpdateIntent(text);
1003
+ }
1004
+ if (Object.keys(patch).length === 0) {
1005
+ await callback?.({
1006
+ text: 'Please specify what to update. Examples:\n- "enable cron job abc-123"\n- "disable cron called daily-check"\n- "change cron abc-123 to run every 2 hours"'
1007
+ });
1008
+ return { success: false, error: "No updates specified" };
1009
+ }
1010
+ const updatedJob = await cronService.updateJob(jobId, patch);
1011
+ const changes = [];
1012
+ if (patch.enabled !== void 0) {
1013
+ changes.push(`status: ${patch.enabled ? "enabled" : "disabled"}`);
1014
+ }
1015
+ if (patch.schedule) {
1016
+ changes.push(`schedule: ${formatSchedule(patch.schedule)}`);
1017
+ }
1018
+ if (patch.name) {
1019
+ changes.push(`name: ${patch.name}`);
1020
+ }
1021
+ const nextRun = updatedJob.state.nextRunAtMs ? new Date(updatedJob.state.nextRunAtMs).toLocaleString() : "not scheduled";
1022
+ await callback?.({
1023
+ text: `Updated cron job "${updatedJob.name}" (${updatedJob.id})
1024
+ Changes: ${changes.join(", ")}
1025
+ Next run: ${nextRun}`
1026
+ });
1027
+ return {
1028
+ success: true,
1029
+ data: {
1030
+ jobId: updatedJob.id,
1031
+ job: updatedJob,
1032
+ changes
1033
+ }
1034
+ };
1035
+ },
1036
+ examples: [
1037
+ [
1038
+ {
1039
+ name: "{{user1}}",
1040
+ content: { text: "Disable the cron job called daily-check" }
1041
+ },
1042
+ {
1043
+ name: "{{agentName}}",
1044
+ content: {
1045
+ text: 'Updated cron job "daily-check" (abc-123)\nChanges: status: disabled\nNext run: not scheduled'
1046
+ }
1047
+ }
1048
+ ],
1049
+ [
1050
+ {
1051
+ name: "{{user1}}",
1052
+ content: { text: "Enable cron abc-123-def-456" }
1053
+ },
1054
+ {
1055
+ name: "{{agentName}}",
1056
+ content: {
1057
+ text: 'Updated cron job "status checker" (abc-123-def-456)\nChanges: status: enabled\nNext run: in 5 minutes'
1058
+ }
1059
+ }
1060
+ ]
1061
+ ]
1062
+ };
1063
+
1064
+ // src/providers/cron-context.ts
1065
+ function formatJobForContext(job) {
1066
+ const scheduleStr = formatSchedule(job.schedule);
1067
+ const nextRun = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "not scheduled";
1068
+ let line = `- ${job.name} (${scheduleStr})`;
1069
+ if (!job.enabled) {
1070
+ line += " [disabled]";
1071
+ } else {
1072
+ line += ` - next: ${nextRun}`;
1073
+ }
1074
+ if (job.state.lastStatus === "error") {
1075
+ line += " [last run failed]";
1076
+ }
1077
+ return line;
1078
+ }
1079
+ var cronContextProvider = {
1080
+ name: "cronContext",
1081
+ description: "Provides information about scheduled cron jobs",
1082
+ dynamic: true,
1083
+ position: 50,
1084
+ // Middle priority
1085
+ get: async (runtime, _message, _state) => {
1086
+ const cronService = runtime.getService(CRON_SERVICE_TYPE);
1087
+ if (!cronService) {
1088
+ return {
1089
+ text: "",
1090
+ values: {
1091
+ hasCronService: false,
1092
+ cronJobCount: 0
1093
+ },
1094
+ data: {
1095
+ available: false
1096
+ }
1097
+ };
1098
+ }
1099
+ const jobs = await cronService.listJobs({ includeDisabled: true });
1100
+ const enabledJobs = jobs.filter((j) => j.enabled);
1101
+ const disabledJobs = jobs.filter((j) => !j.enabled);
1102
+ const nowMs = Date.now();
1103
+ const oneHourFromNow = nowMs + 36e5;
1104
+ const upcomingJobs = enabledJobs.filter(
1105
+ (j) => j.state.nextRunAtMs && j.state.nextRunAtMs <= oneHourFromNow
1106
+ );
1107
+ const recentJobs = jobs.filter(
1108
+ (j) => j.state.lastRunAtMs && j.state.lastRunAtMs >= nowMs - 36e5
1109
+ );
1110
+ const failedJobs = jobs.filter((j) => j.state.lastStatus === "error");
1111
+ const lines = [];
1112
+ if (jobs.length === 0) {
1113
+ lines.push("No cron jobs are scheduled.");
1114
+ } else {
1115
+ lines.push(`Scheduled Jobs (${enabledJobs.length} active, ${disabledJobs.length} disabled):`);
1116
+ if (upcomingJobs.length > 0) {
1117
+ lines.push("\nUpcoming (next hour):");
1118
+ for (const job of upcomingJobs.slice(0, 5)) {
1119
+ lines.push(formatJobForContext(job));
1120
+ }
1121
+ if (upcomingJobs.length > 5) {
1122
+ lines.push(` ... and ${upcomingJobs.length - 5} more`);
1123
+ }
1124
+ }
1125
+ if (failedJobs.length > 0) {
1126
+ lines.push("\nRecently failed:");
1127
+ for (const job of failedJobs.slice(0, 3)) {
1128
+ lines.push(`- ${job.name}: ${job.state.lastError || "unknown error"}`);
1129
+ }
1130
+ }
1131
+ if (enabledJobs.length <= 10 && enabledJobs.length > 0) {
1132
+ lines.push("\nAll active jobs:");
1133
+ for (const job of enabledJobs) {
1134
+ lines.push(formatJobForContext(job));
1135
+ }
1136
+ } else if (enabledJobs.length > 10) {
1137
+ lines.push(`
1138
+ ${enabledJobs.length} active jobs total. Use "list crons" to see all.`);
1139
+ }
1140
+ }
1141
+ return {
1142
+ text: lines.join("\n"),
1143
+ values: {
1144
+ hasCronService: true,
1145
+ cronJobCount: jobs.length,
1146
+ enabledJobCount: enabledJobs.length,
1147
+ disabledJobCount: disabledJobs.length,
1148
+ upcomingJobCount: upcomingJobs.length,
1149
+ failedJobCount: failedJobs.length
1150
+ },
1151
+ data: {
1152
+ available: true,
1153
+ jobs: jobs.map((j) => ({
1154
+ id: j.id,
1155
+ name: j.name,
1156
+ enabled: j.enabled,
1157
+ schedule: j.schedule,
1158
+ nextRunAtMs: j.state.nextRunAtMs,
1159
+ lastStatus: j.state.lastStatus
1160
+ })),
1161
+ upcoming: upcomingJobs.map((j) => j.id),
1162
+ failed: failedJobs.map((j) => j.id),
1163
+ recent: recentJobs.map((j) => j.id)
1164
+ }
1165
+ };
1166
+ }
1167
+ };
1168
+
1169
+ // src/routes/index.ts
1170
+ function getCronService(runtime) {
1171
+ const svc = runtime.getService(CRON_SERVICE_TYPE);
1172
+ if (!svc) {
1173
+ throw new Error("CronService not available");
1174
+ }
1175
+ return svc;
1176
+ }
1177
+ async function handleCronStatus(_req, res, runtime) {
1178
+ const svc = getCronService(runtime);
1179
+ const status = await svc.getStatus();
1180
+ res.json({
1181
+ enabled: status.initialized,
1182
+ jobs: status.jobCount,
1183
+ tracked: status.trackedJobCount,
1184
+ config: status.config
1185
+ });
1186
+ }
1187
+ async function handleCronList(req, res, runtime) {
1188
+ const includeDisabled = req.body?.includeDisabled === true;
1189
+ const svc = getCronService(runtime);
1190
+ const jobs = await svc.listJobs({ includeDisabled });
1191
+ res.json({ jobs });
1192
+ }
1193
+ async function handleCronAdd(req, res, runtime) {
1194
+ const body = req.body ?? {};
1195
+ const normalized = normalizeCronJobCreate(body);
1196
+ if (!normalized) {
1197
+ res.status(400).json({ error: "Invalid job input" });
1198
+ return;
1199
+ }
1200
+ const svc = getCronService(runtime);
1201
+ const job = await svc.createJob(normalized);
1202
+ res.json({ job });
1203
+ }
1204
+ async function handleCronUpdate(req, res, runtime) {
1205
+ const body = req.body ?? {};
1206
+ const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
1207
+ if (!jobId) {
1208
+ res.status(400).json({ error: "Missing jobId" });
1209
+ return;
1210
+ }
1211
+ const patch = normalizeCronJobPatch(body.patch ?? body);
1212
+ if (!patch) {
1213
+ res.status(400).json({ error: "Invalid patch" });
1214
+ return;
1215
+ }
1216
+ const svc = getCronService(runtime);
1217
+ const job = await svc.updateJob(jobId, patch);
1218
+ res.json({ job });
1219
+ }
1220
+ async function handleCronRemove(req, res, runtime) {
1221
+ const body = req.body ?? {};
1222
+ const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
1223
+ if (!jobId) {
1224
+ res.status(400).json({ error: "Missing jobId" });
1225
+ return;
1226
+ }
1227
+ const svc = getCronService(runtime);
1228
+ const deleted = await svc.deleteJob(jobId);
1229
+ res.json({ deleted });
1230
+ }
1231
+ async function handleCronRun(req, res, runtime) {
1232
+ const body = req.body ?? {};
1233
+ const jobId = typeof body.jobId === "string" ? body.jobId : typeof body.id === "string" ? body.id : "";
1234
+ if (!jobId) {
1235
+ res.status(400).json({ error: "Missing jobId" });
1236
+ return;
1237
+ }
1238
+ const mode = body.mode === "due" ? "due" : "force";
1239
+ const svc = getCronService(runtime);
1240
+ const result = await svc.runJob(jobId, mode);
1241
+ res.json(result);
1242
+ }
1243
+ async function handleCronRuns(req, res, _runtime) {
1244
+ const body = req.body ?? {};
1245
+ const jobId = typeof body.id === "string" ? body.id : "";
1246
+ const limit = typeof body.limit === "number" ? body.limit : 50;
1247
+ if (!jobId) {
1248
+ res.status(400).json({ error: "Missing id" });
1249
+ return;
1250
+ }
1251
+ const storePath = resolveCronStorePath();
1252
+ if (!storePath) {
1253
+ res.json({ entries: [] });
1254
+ return;
1255
+ }
1256
+ const logPath = resolveCronRunLogPath({ storePath, jobId });
1257
+ const entries = await readCronRunLogEntries(logPath, { limit, jobId });
1258
+ res.json({ entries });
1259
+ }
1260
+ var cronRoutes = [
1261
+ { type: "POST", path: "/api/cron/status", handler: handleCronStatus },
1262
+ { type: "POST", path: "/api/cron/list", handler: handleCronList },
1263
+ { type: "POST", path: "/api/cron/add", handler: handleCronAdd },
1264
+ { type: "POST", path: "/api/cron/update", handler: handleCronUpdate },
1265
+ { type: "POST", path: "/api/cron/remove", handler: handleCronRemove },
1266
+ { type: "POST", path: "/api/cron/run", handler: handleCronRun },
1267
+ { type: "POST", path: "/api/cron/runs", handler: handleCronRuns }
1268
+ ];
1269
+
1270
+ // src/services/cron-service.ts
1271
+ import { logger, Service } from "@elizaos/core";
1272
+ import { v4 as uuidv43 } from "uuid";
1273
+
1274
+ // src/executor/job-executor.ts
1275
+ import { v4 as uuidv4 } from "uuid";
1276
+ function createTimeoutController(timeoutMs) {
1277
+ const controller = new AbortController();
1278
+ const timer = setTimeout(() => {
1279
+ controller.abort(new Error("Job execution timeout"));
1280
+ }, timeoutMs);
1281
+ return {
1282
+ controller,
1283
+ cleanup: () => clearTimeout(timer)
1284
+ };
1285
+ }
1286
+ async function withEnforcedTimeout(operation, timeoutMs, signal) {
1287
+ if (signal.aborted) {
1288
+ throw new Error("Job execution timeout");
1289
+ }
1290
+ const timeoutPromise = new Promise((_, reject) => {
1291
+ const timeoutId = setTimeout(() => {
1292
+ reject(new Error("Job execution timeout"));
1293
+ }, timeoutMs);
1294
+ signal.addEventListener("abort", () => {
1295
+ clearTimeout(timeoutId);
1296
+ reject(new Error("Job execution timeout"));
1297
+ });
1298
+ });
1299
+ return Promise.race([operation(), timeoutPromise]);
1300
+ }
1301
+ async function executePromptPayload(runtime, payload, context) {
1302
+ const cronContext = `[Cron Job: ${context.job.name}]${context.job.description ? ` - ${context.job.description}` : ""}`;
1303
+ const fullPrompt = `${cronContext}
1304
+
1305
+ ${payload.text}`;
1306
+ const result = await runtime.useModel("TEXT_LARGE", {
1307
+ prompt: fullPrompt
1308
+ });
1309
+ return result;
1310
+ }
1311
+ async function executeActionPayload(runtime, payload, context) {
1312
+ const actions = runtime.actions ?? [];
1313
+ const action = actions.find((a) => a.name.toLowerCase() === payload.actionName.toLowerCase());
1314
+ if (!action) {
1315
+ throw new Error(`Action not found: ${payload.actionName}`);
1316
+ }
1317
+ const roomId = payload.roomId || runtime.agentId;
1318
+ const memory = {
1319
+ id: uuidv4(),
1320
+ entityId: runtime.agentId,
1321
+ roomId,
1322
+ agentId: runtime.agentId,
1323
+ content: {
1324
+ text: `[Cron Job: ${context.job.name}] Executing action: ${payload.actionName}`,
1325
+ // Spread params into content for actions to access
1326
+ ...payload.params
1327
+ },
1328
+ createdAt: Date.now()
1329
+ };
1330
+ const callbackResponses = [];
1331
+ const callback = async (response) => {
1332
+ if (response.text) {
1333
+ callbackResponses.push(response.text);
1334
+ }
1335
+ return [];
1336
+ };
1337
+ const isValid = await action.validate(runtime, memory, void 0);
1338
+ if (!isValid) {
1339
+ throw new Error(`Action validation failed: ${payload.actionName}`);
1340
+ }
1341
+ const handlerResult = await action.handler(runtime, memory, void 0, void 0, callback);
1342
+ const outputParts = [];
1343
+ if (callbackResponses.length > 0) {
1344
+ outputParts.push(...callbackResponses);
1345
+ }
1346
+ if (handlerResult !== void 0 && handlerResult !== null) {
1347
+ if (typeof handlerResult === "string") {
1348
+ outputParts.push(handlerResult);
1349
+ } else if (typeof handlerResult === "object") {
1350
+ const result = handlerResult;
1351
+ if (result.text && typeof result.text === "string") {
1352
+ outputParts.push(result.text);
1353
+ } else if (result.success !== void 0 || result.data !== void 0) {
1354
+ outputParts.push(JSON.stringify(handlerResult));
1355
+ }
1356
+ }
1357
+ }
1358
+ return outputParts.join("\n") || `Action ${payload.actionName} completed`;
1359
+ }
1360
+ async function executeEventPayload(runtime, payload, context) {
1361
+ const eventPayload = {
1362
+ runtime,
1363
+ source: `cron:${context.job.id}`,
1364
+ cronJob: {
1365
+ id: context.job.id,
1366
+ name: context.job.name
1367
+ },
1368
+ ...payload.payload || {}
1369
+ };
1370
+ await runtime.emitEvent(payload.eventName, eventPayload);
1371
+ return `Event ${payload.eventName} emitted`;
1372
+ }
1373
+ async function executeJob(runtime, job, config) {
1374
+ const payloadRecord = job.payload;
1375
+ if (isOttoPayload(payloadRecord)) {
1376
+ return executeOttoJob(runtime, job, config);
1377
+ }
1378
+ const startedAtMs = Date.now();
1379
+ let timeoutMs = config.defaultTimeoutMs ?? DEFAULT_CRON_CONFIG.defaultTimeoutMs;
1380
+ if (job.payload.kind === "prompt" && job.payload.timeoutSeconds) {
1381
+ timeoutMs = job.payload.timeoutSeconds * 1e3;
1382
+ }
1383
+ const { controller, cleanup } = createTimeoutController(timeoutMs);
1384
+ const context = {
1385
+ job,
1386
+ startedAtMs,
1387
+ signal: controller.signal
1388
+ };
1389
+ let status = "ok";
1390
+ let output;
1391
+ let error;
1392
+ try {
1393
+ if (controller.signal.aborted) {
1394
+ throw new Error("Job execution timeout");
1395
+ }
1396
+ const executeOperation = async () => {
1397
+ switch (job.payload.kind) {
1398
+ case "prompt":
1399
+ return executePromptPayload(runtime, job.payload, context);
1400
+ case "action":
1401
+ return executeActionPayload(runtime, job.payload, context);
1402
+ case "event":
1403
+ return executeEventPayload(runtime, job.payload, context);
1404
+ default: {
1405
+ const _exhaustive = job.payload;
1406
+ throw new Error(`Unknown payload kind: ${job.payload.kind}`);
1407
+ }
1408
+ }
1409
+ };
1410
+ output = await withEnforcedTimeout(executeOperation, timeoutMs, controller.signal);
1411
+ } catch (err) {
1412
+ if (err instanceof Error) {
1413
+ if (err.message === "Job execution timeout" || controller.signal.aborted) {
1414
+ status = "timeout";
1415
+ error = "Execution timed out";
1416
+ } else {
1417
+ status = "error";
1418
+ error = err.message;
1419
+ }
1420
+ } else {
1421
+ status = "error";
1422
+ error = String(err);
1423
+ }
1424
+ } finally {
1425
+ cleanup();
1426
+ }
1427
+ const durationMs = Date.now() - startedAtMs;
1428
+ return {
1429
+ status,
1430
+ durationMs,
1431
+ output,
1432
+ error
1433
+ };
1434
+ }
1435
+ function validateJobExecutability(runtime, job) {
1436
+ const { payload } = job;
1437
+ const payloadRecord = payload;
1438
+ if (isOttoPayload(payloadRecord)) {
1439
+ if (payloadRecord.kind === "systemEvent") {
1440
+ const text = typeof payloadRecord.text === "string" ? payloadRecord.text.trim() : "";
1441
+ return text ? null : "systemEvent payload must have non-empty text";
1442
+ }
1443
+ if (payloadRecord.kind === "agentTurn") {
1444
+ const message = typeof payloadRecord.message === "string" ? payloadRecord.message.trim() : "";
1445
+ return message ? null : "agentTurn payload must have non-empty message";
1446
+ }
1447
+ return null;
1448
+ }
1449
+ switch (payload.kind) {
1450
+ case "prompt": {
1451
+ const text = payload.text?.trim();
1452
+ if (!text) {
1453
+ return "Prompt payload must have non-empty text";
1454
+ }
1455
+ if (typeof runtime.useModel !== "function") {
1456
+ return "Runtime does not support useModel for prompt execution";
1457
+ }
1458
+ return null;
1459
+ }
1460
+ case "event": {
1461
+ const eventName = payload.eventName?.trim();
1462
+ if (!eventName) {
1463
+ return "Event payload must have non-empty eventName";
1464
+ }
1465
+ if (typeof runtime.emitEvent !== "function") {
1466
+ return "Runtime does not support emitEvent for event execution";
1467
+ }
1468
+ return null;
1469
+ }
1470
+ case "action": {
1471
+ const actionName = payload.actionName?.trim();
1472
+ if (!actionName) {
1473
+ return "Action payload must have non-empty actionName";
1474
+ }
1475
+ const actions = runtime.actions ?? [];
1476
+ const action = actions.find((a) => a.name.toLowerCase() === actionName.toLowerCase());
1477
+ return action ? null : `Action not found: ${payload.actionName}`;
1478
+ }
1479
+ default: {
1480
+ return `Unknown payload kind: ${payload.kind}`;
1481
+ }
1482
+ }
1483
+ }
1484
+
1485
+ // src/scheduler/timer-manager.ts
1486
+ import { Cron as Cron2 } from "croner";
1487
+ var TimerManager = class {
1488
+ config;
1489
+ onJobDue;
1490
+ trackedJobs = /* @__PURE__ */ new Map();
1491
+ checkInterval = null;
1492
+ running = false;
1493
+ constructor(config, onJobDue) {
1494
+ this.config = config;
1495
+ this.onJobDue = onJobDue;
1496
+ }
1497
+ /**
1498
+ * Starts the timer manager
1499
+ */
1500
+ start() {
1501
+ if (this.running) {
1502
+ return;
1503
+ }
1504
+ this.running = true;
1505
+ this.startCheckInterval();
1506
+ }
1507
+ /**
1508
+ * Stops the timer manager and cleans up all timers
1509
+ */
1510
+ stop() {
1511
+ this.running = false;
1512
+ this.stopCheckInterval();
1513
+ this.clearAllJobs();
1514
+ }
1515
+ /**
1516
+ * Adds or updates a job in the timer manager
1517
+ * @param job The job to track
1518
+ */
1519
+ trackJob(job) {
1520
+ this.untrackJob(job.id);
1521
+ if (!job.enabled) {
1522
+ return;
1523
+ }
1524
+ const tracked = {
1525
+ job,
1526
+ executing: false
1527
+ };
1528
+ if (job.schedule.kind === "cron") {
1529
+ const cronInstance = new Cron2(job.schedule.expr, {
1530
+ timezone: job.schedule.tz?.trim() || void 0,
1531
+ catch: true,
1532
+ paused: true
1533
+ // We manage firing manually via the check interval
1534
+ });
1535
+ tracked.cronInstance = cronInstance;
1536
+ }
1537
+ tracked.nextRunAtMs = job.state.nextRunAtMs ?? computeNextRunAtMs(job.schedule, Date.now());
1538
+ this.trackedJobs.set(job.id, tracked);
1539
+ }
1540
+ /**
1541
+ * Removes a job from tracking
1542
+ * @param jobId The job ID to remove
1543
+ */
1544
+ untrackJob(jobId) {
1545
+ const tracked = this.trackedJobs.get(jobId);
1546
+ if (tracked) {
1547
+ if (tracked.cronInstance) {
1548
+ tracked.cronInstance.stop();
1549
+ }
1550
+ this.trackedJobs.delete(jobId);
1551
+ }
1552
+ }
1553
+ /**
1554
+ * Marks a job as currently executing (to prevent overlapping executions)
1555
+ * @param jobId The job ID
1556
+ */
1557
+ markExecuting(jobId) {
1558
+ const tracked = this.trackedJobs.get(jobId);
1559
+ if (tracked) {
1560
+ tracked.executing = true;
1561
+ }
1562
+ }
1563
+ /**
1564
+ * Marks a job as finished executing and recalculates next run
1565
+ * @param jobId The job ID
1566
+ * @param updatedJob The job with updated state (optional)
1567
+ */
1568
+ markFinished(jobId, updatedJob) {
1569
+ const tracked = this.trackedJobs.get(jobId);
1570
+ if (tracked) {
1571
+ tracked.executing = false;
1572
+ if (updatedJob) {
1573
+ tracked.job = updatedJob;
1574
+ tracked.nextRunAtMs = computeNextRunAtMs(updatedJob.schedule, Date.now());
1575
+ }
1576
+ }
1577
+ }
1578
+ /**
1579
+ * Gets the next scheduled run time for a job
1580
+ * @param jobId The job ID
1581
+ * @returns Next run time in ms, or undefined
1582
+ */
1583
+ getNextRunAtMs(jobId) {
1584
+ return this.trackedJobs.get(jobId)?.nextRunAtMs;
1585
+ }
1586
+ /**
1587
+ * Gets all tracked job IDs
1588
+ */
1589
+ getTrackedJobIds() {
1590
+ return Array.from(this.trackedJobs.keys());
1591
+ }
1592
+ /**
1593
+ * Gets the count of tracked jobs
1594
+ */
1595
+ getTrackedJobCount() {
1596
+ return this.trackedJobs.size;
1597
+ }
1598
+ /**
1599
+ * Checks if a specific job is currently executing
1600
+ */
1601
+ isJobExecuting(jobId) {
1602
+ return this.trackedJobs.get(jobId)?.executing ?? false;
1603
+ }
1604
+ /**
1605
+ * Forces an immediate check for due jobs
1606
+ */
1607
+ checkNow() {
1608
+ if (this.running) {
1609
+ this.performCheck();
1610
+ }
1611
+ }
1612
+ /**
1613
+ * Starts the periodic check interval
1614
+ */
1615
+ startCheckInterval() {
1616
+ if (this.checkInterval) {
1617
+ return;
1618
+ }
1619
+ const intervalMs = this.config.timerCheckIntervalMs ?? DEFAULT_CRON_CONFIG.timerCheckIntervalMs;
1620
+ this.checkInterval = setInterval(() => {
1621
+ this.performCheck();
1622
+ }, intervalMs);
1623
+ if (this.checkInterval.unref) {
1624
+ this.checkInterval.unref();
1625
+ }
1626
+ }
1627
+ /**
1628
+ * Stops the periodic check interval
1629
+ */
1630
+ stopCheckInterval() {
1631
+ if (this.checkInterval) {
1632
+ clearInterval(this.checkInterval);
1633
+ this.checkInterval = null;
1634
+ }
1635
+ }
1636
+ /**
1637
+ * Performs a check for all due jobs
1638
+ */
1639
+ performCheck() {
1640
+ const nowMs = Date.now();
1641
+ const dueJobs = [];
1642
+ for (const [jobId, tracked] of this.trackedJobs) {
1643
+ if (tracked.executing) {
1644
+ continue;
1645
+ }
1646
+ if (!tracked.job.enabled) {
1647
+ continue;
1648
+ }
1649
+ if (isJobDue(tracked.nextRunAtMs, nowMs)) {
1650
+ dueJobs.push(jobId);
1651
+ }
1652
+ }
1653
+ for (const jobId of dueJobs) {
1654
+ const tracked = this.trackedJobs.get(jobId);
1655
+ if (tracked && !tracked.executing) {
1656
+ tracked.executing = true;
1657
+ this.onJobDue(jobId).catch((error) => {
1658
+ console.error(`[CronTimerManager] Error executing job ${jobId}:`, error);
1659
+ });
1660
+ }
1661
+ }
1662
+ }
1663
+ /**
1664
+ * Clears all tracked jobs
1665
+ */
1666
+ clearAllJobs() {
1667
+ for (const [, tracked] of this.trackedJobs) {
1668
+ if (tracked.cronInstance) {
1669
+ tracked.cronInstance.stop();
1670
+ }
1671
+ }
1672
+ this.trackedJobs.clear();
1673
+ }
1674
+ };
1675
+
1676
+ // src/storage/cron-storage.ts
1677
+ import { v4 as uuidv42 } from "uuid";
1678
+ var AsyncMutex = class {
1679
+ queue = [];
1680
+ locked = false;
1681
+ async acquire() {
1682
+ return new Promise((resolve) => {
1683
+ const tryAcquire = () => {
1684
+ if (!this.locked) {
1685
+ this.locked = true;
1686
+ resolve(() => this.release());
1687
+ } else {
1688
+ this.queue.push(tryAcquire);
1689
+ }
1690
+ };
1691
+ tryAcquire();
1692
+ });
1693
+ }
1694
+ release() {
1695
+ this.locked = false;
1696
+ const next = this.queue.shift();
1697
+ if (next) {
1698
+ next();
1699
+ }
1700
+ }
1701
+ };
1702
+ var indexMutexes = /* @__PURE__ */ new Map();
1703
+ function getIndexMutex(agentId) {
1704
+ let mutex = indexMutexes.get(agentId);
1705
+ if (!mutex) {
1706
+ mutex = new AsyncMutex();
1707
+ indexMutexes.set(agentId, mutex);
1708
+ }
1709
+ return mutex;
1710
+ }
1711
+ function getJobComponentType(jobId) {
1712
+ return `${CRON_JOB_COMPONENT_PREFIX}:${jobId}`;
1713
+ }
1714
+ async function getJobIndex(runtime) {
1715
+ const component = await runtime.getComponent(runtime.agentId, CRON_JOB_INDEX_COMPONENT);
1716
+ if (!component) {
1717
+ return { jobIds: [] };
1718
+ }
1719
+ return component.data;
1720
+ }
1721
+ async function saveJobIndex(runtime, index) {
1722
+ const existing = await runtime.getComponent(runtime.agentId, CRON_JOB_INDEX_COMPONENT);
1723
+ const component = {
1724
+ id: existing?.id || uuidv42(),
1725
+ entityId: runtime.agentId,
1726
+ agentId: runtime.agentId,
1727
+ roomId: runtime.agentId,
1728
+ // Use agentId as room for agent-scoped data
1729
+ worldId: existing?.worldId || uuidv42(),
1730
+ sourceEntityId: runtime.agentId,
1731
+ type: CRON_JOB_INDEX_COMPONENT,
1732
+ createdAt: existing?.createdAt || Date.now(),
1733
+ data: index
1734
+ };
1735
+ if (existing) {
1736
+ await runtime.updateComponent(component);
1737
+ } else {
1738
+ await runtime.createComponent(component);
1739
+ }
1740
+ }
1741
+ async function addToIndex(runtime, jobId) {
1742
+ const mutex = getIndexMutex(runtime.agentId);
1743
+ const release = await mutex.acquire();
1744
+ try {
1745
+ const index = await getJobIndex(runtime);
1746
+ if (!index.jobIds.includes(jobId)) {
1747
+ index.jobIds.push(jobId);
1748
+ await saveJobIndex(runtime, index);
1749
+ }
1750
+ } finally {
1751
+ release();
1752
+ }
1753
+ }
1754
+ async function removeFromIndex(runtime, jobId) {
1755
+ const mutex = getIndexMutex(runtime.agentId);
1756
+ const release = await mutex.acquire();
1757
+ try {
1758
+ const index = await getJobIndex(runtime);
1759
+ const filtered = index.jobIds.filter((id) => id !== jobId);
1760
+ if (filtered.length !== index.jobIds.length) {
1761
+ index.jobIds = filtered;
1762
+ await saveJobIndex(runtime, index);
1763
+ }
1764
+ } finally {
1765
+ release();
1766
+ }
1767
+ }
1768
+ function validateJobData(data) {
1769
+ if (!data || typeof data !== "object") {
1770
+ return null;
1771
+ }
1772
+ const obj = data;
1773
+ if (typeof obj.id !== "string" || !obj.id) {
1774
+ return null;
1775
+ }
1776
+ if (typeof obj.name !== "string" || !obj.name) {
1777
+ return null;
1778
+ }
1779
+ if (typeof obj.createdAtMs !== "number") {
1780
+ return null;
1781
+ }
1782
+ if (typeof obj.updatedAtMs !== "number") {
1783
+ return null;
1784
+ }
1785
+ if (!obj.schedule || typeof obj.schedule !== "object") {
1786
+ return null;
1787
+ }
1788
+ const schedule = obj.schedule;
1789
+ const validScheduleKinds = ["at", "every", "cron"];
1790
+ if (!validScheduleKinds.includes(schedule.kind)) {
1791
+ return null;
1792
+ }
1793
+ if (!obj.payload || typeof obj.payload !== "object") {
1794
+ return null;
1795
+ }
1796
+ const payload = obj.payload;
1797
+ const validPayloadKinds = ["prompt", "action", "event"];
1798
+ if (!validPayloadKinds.includes(payload.kind)) {
1799
+ return null;
1800
+ }
1801
+ if (!obj.state || typeof obj.state !== "object") {
1802
+ return null;
1803
+ }
1804
+ const state = obj.state;
1805
+ if (typeof state.runCount !== "number" || typeof state.errorCount !== "number") {
1806
+ return null;
1807
+ }
1808
+ const job = {
1809
+ ...data,
1810
+ enabled: typeof obj.enabled === "boolean" ? obj.enabled : true,
1811
+ deleteAfterRun: typeof obj.deleteAfterRun === "boolean" ? obj.deleteAfterRun : false
1812
+ };
1813
+ return job;
1814
+ }
1815
+ function matchesFilter(job, filter) {
1816
+ if (!filter) return true;
1817
+ if (filter.enabled !== void 0) {
1818
+ if (job.enabled !== filter.enabled) return false;
1819
+ } else if (!filter.includeDisabled && !job.enabled) {
1820
+ return false;
1821
+ }
1822
+ if (filter.tags?.length) {
1823
+ if (!job.tags?.some((tag) => filter.tags?.includes(tag))) return false;
1824
+ }
1825
+ return true;
1826
+ }
1827
+ function getCronStorage(runtime) {
1828
+ return {
1829
+ async getJob(jobId) {
1830
+ const componentType = getJobComponentType(jobId);
1831
+ const component = await runtime.getComponent(runtime.agentId, componentType);
1832
+ if (!component) {
1833
+ return null;
1834
+ }
1835
+ const validatedJob = validateJobData(component.data);
1836
+ if (!validatedJob) {
1837
+ console.warn(`[cron-storage] Invalid job data for ${jobId}, skipping`);
1838
+ return null;
1839
+ }
1840
+ return validatedJob;
1841
+ },
1842
+ async saveJob(job) {
1843
+ const componentType = getJobComponentType(job.id);
1844
+ const existing = await runtime.getComponent(runtime.agentId, componentType);
1845
+ const component = {
1846
+ id: existing?.id || uuidv42(),
1847
+ entityId: runtime.agentId,
1848
+ agentId: runtime.agentId,
1849
+ roomId: runtime.agentId,
1850
+ worldId: existing?.worldId || uuidv42(),
1851
+ sourceEntityId: runtime.agentId,
1852
+ type: componentType,
1853
+ createdAt: existing?.createdAt || job.createdAtMs,
1854
+ data: job
1855
+ };
1856
+ if (existing) {
1857
+ await runtime.updateComponent(component);
1858
+ } else {
1859
+ await runtime.createComponent(component);
1860
+ await addToIndex(runtime, job.id);
1861
+ }
1862
+ },
1863
+ async deleteJob(jobId) {
1864
+ const componentType = getJobComponentType(jobId);
1865
+ const existing = await runtime.getComponent(runtime.agentId, componentType);
1866
+ if (!existing) {
1867
+ return false;
1868
+ }
1869
+ await runtime.deleteComponent(existing.id);
1870
+ await removeFromIndex(runtime, jobId);
1871
+ return true;
1872
+ },
1873
+ async listJobs(filter) {
1874
+ const index = await getJobIndex(runtime);
1875
+ const jobs = [];
1876
+ for (const jobId of index.jobIds) {
1877
+ const job = await this.getJob(jobId);
1878
+ if (job && matchesFilter(job, filter)) {
1879
+ jobs.push(job);
1880
+ }
1881
+ }
1882
+ jobs.sort((a, b) => {
1883
+ const aNext = a.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
1884
+ const bNext = b.state.nextRunAtMs ?? Number.MAX_SAFE_INTEGER;
1885
+ return aNext - bNext;
1886
+ });
1887
+ return jobs;
1888
+ },
1889
+ async getJobCount() {
1890
+ const index = await getJobIndex(runtime);
1891
+ return index.jobIds.length;
1892
+ },
1893
+ async hasJob(jobId) {
1894
+ const index = await getJobIndex(runtime);
1895
+ return index.jobIds.includes(jobId);
1896
+ }
1897
+ };
1898
+ }
1899
+
1900
+ // src/services/cron-service.ts
1901
+ var CronService = class _CronService extends Service {
1902
+ static serviceType = CRON_SERVICE_TYPE;
1903
+ capabilityDescription = "Schedules and executes recurring or one-time cron jobs";
1904
+ cronConfig;
1905
+ storage;
1906
+ timerManager;
1907
+ initialized = false;
1908
+ constructor(runtime, config) {
1909
+ super(runtime);
1910
+ this.cronConfig = { ...DEFAULT_CRON_CONFIG, ...config };
1911
+ }
1912
+ /**
1913
+ * Starts the cron service
1914
+ */
1915
+ static async start(runtime) {
1916
+ const service = new _CronService(runtime);
1917
+ await service.initialize();
1918
+ return service;
1919
+ }
1920
+ /**
1921
+ * Initializes the service, loading existing jobs and starting timers
1922
+ */
1923
+ async initialize() {
1924
+ if (this.initialized) {
1925
+ return;
1926
+ }
1927
+ this.storage = getCronStorage(this.runtime);
1928
+ this.timerManager = new TimerManager(this.cronConfig, async (jobId) => {
1929
+ await this.handleJobDue(jobId);
1930
+ });
1931
+ const jobs = await this.storage.listJobs({ includeDisabled: true });
1932
+ for (const job of jobs) {
1933
+ const nowMs = Date.now();
1934
+ const nextRunAtMs = computeNextRunAtMs(job.schedule, nowMs);
1935
+ if (job.state.nextRunAtMs !== nextRunAtMs) {
1936
+ job.state.nextRunAtMs = nextRunAtMs;
1937
+ await this.storage.saveJob(job);
1938
+ }
1939
+ if (job.enabled) {
1940
+ this.timerManager.trackJob(job);
1941
+ }
1942
+ }
1943
+ if (this.cronConfig.catchUpMissedJobs) {
1944
+ await this.handleMissedJobs(jobs);
1945
+ }
1946
+ this.timerManager.start();
1947
+ await startHeartbeat(this.runtime);
1948
+ this.initialized = true;
1949
+ logger.info(`[CronService] Started for agent ${this.runtime.agentId} with ${jobs.length} jobs`);
1950
+ }
1951
+ /**
1952
+ * Stops the cron service
1953
+ */
1954
+ async stop() {
1955
+ if (this.timerManager) {
1956
+ this.timerManager.stop();
1957
+ }
1958
+ this.initialized = false;
1959
+ logger.info(`[CronService] Stopped for agent ${this.runtime.agentId}`);
1960
+ }
1961
+ /**
1962
+ * Gets the service configuration
1963
+ */
1964
+ getConfig() {
1965
+ return { ...this.cronConfig };
1966
+ }
1967
+ // ============================================================================
1968
+ // CRUD OPERATIONS
1969
+ // ============================================================================
1970
+ /**
1971
+ * Creates a new cron job
1972
+ * @param input Job creation input
1973
+ * @returns The created job
1974
+ * @throws Error if validation fails or max jobs exceeded
1975
+ */
1976
+ async createJob(input) {
1977
+ const scheduleError = validateSchedule(input.schedule, this.cronConfig);
1978
+ if (scheduleError) {
1979
+ throw new Error(`Invalid schedule: ${scheduleError}`);
1980
+ }
1981
+ const currentCount = await this.storage.getJobCount();
1982
+ if (currentCount >= this.cronConfig.maxJobsPerAgent) {
1983
+ throw new Error(
1984
+ `Maximum jobs limit reached (${this.cronConfig.maxJobsPerAgent}). Delete some jobs before creating new ones.`
1985
+ );
1986
+ }
1987
+ const nowMs = Date.now();
1988
+ const job = {
1989
+ id: uuidv43(),
1990
+ name: input.name,
1991
+ description: input.description,
1992
+ // Explicitly default to true if not provided - jobs are enabled by default
1993
+ enabled: input.enabled ?? true,
1994
+ // Explicitly default to false - jobs persist after run by default
1995
+ deleteAfterRun: input.deleteAfterRun ?? false,
1996
+ createdAtMs: nowMs,
1997
+ updatedAtMs: nowMs,
1998
+ schedule: input.schedule,
1999
+ payload: input.payload,
2000
+ tags: input.tags,
2001
+ metadata: input.metadata,
2002
+ state: {
2003
+ nextRunAtMs: computeNextRunAtMs(input.schedule, nowMs),
2004
+ runCount: input.state?.runCount ?? 0,
2005
+ errorCount: input.state?.errorCount ?? 0,
2006
+ ...input.state
2007
+ }
2008
+ };
2009
+ const execError = validateJobExecutability(this.runtime, job);
2010
+ if (execError) {
2011
+ throw new Error(`Job cannot be executed: ${execError}`);
2012
+ }
2013
+ await this.storage.saveJob(job);
2014
+ if (job.enabled) {
2015
+ this.timerManager.trackJob(job);
2016
+ }
2017
+ await this.emitCronEvent(CronEvents.CRON_CREATED, job);
2018
+ logger.info(
2019
+ `[CronService] Created job "${job.name}" (${job.id}) - ${formatSchedule(job.schedule)}`
2020
+ );
2021
+ return job;
2022
+ }
2023
+ /**
2024
+ * Updates an existing cron job
2025
+ * @param jobId The job ID to update
2026
+ * @param patch The fields to update
2027
+ * @returns The updated job
2028
+ * @throws Error if job not found or validation fails
2029
+ */
2030
+ async updateJob(jobId, patch) {
2031
+ const existing = await this.storage.getJob(jobId);
2032
+ if (!existing) {
2033
+ throw new Error(`Job not found: ${jobId}`);
2034
+ }
2035
+ if (patch.schedule) {
2036
+ const scheduleError = validateSchedule(patch.schedule, this.cronConfig);
2037
+ if (scheduleError) {
2038
+ throw new Error(`Invalid schedule: ${scheduleError}`);
2039
+ }
2040
+ }
2041
+ const nowMs = Date.now();
2042
+ const updated = {
2043
+ ...existing,
2044
+ ...patch,
2045
+ id: existing.id,
2046
+ // Ensure ID is immutable
2047
+ createdAtMs: existing.createdAtMs,
2048
+ // Ensure createdAt is immutable
2049
+ updatedAtMs: nowMs,
2050
+ state: {
2051
+ ...existing.state,
2052
+ ...patch.state
2053
+ }
2054
+ };
2055
+ if (patch.schedule || patch.enabled === true && !existing.enabled) {
2056
+ updated.state.nextRunAtMs = computeNextRunAtMs(updated.schedule, nowMs);
2057
+ }
2058
+ if (patch.enabled === false) {
2059
+ updated.state.nextRunAtMs = void 0;
2060
+ updated.state.runningAtMs = void 0;
2061
+ }
2062
+ if (patch.payload) {
2063
+ const execError = validateJobExecutability(this.runtime, updated);
2064
+ if (execError) {
2065
+ throw new Error(`Job cannot be executed: ${execError}`);
2066
+ }
2067
+ }
2068
+ await this.storage.saveJob(updated);
2069
+ if (updated.enabled) {
2070
+ this.timerManager.trackJob(updated);
2071
+ } else {
2072
+ this.timerManager.untrackJob(jobId);
2073
+ }
2074
+ await this.emitCronEvent(CronEvents.CRON_UPDATED, updated);
2075
+ logger.info(`[CronService] Updated job "${updated.name}" (${updated.id})`);
2076
+ return updated;
2077
+ }
2078
+ /**
2079
+ * Deletes a cron job
2080
+ * @param jobId The job ID to delete
2081
+ * @returns true if deleted, false if not found
2082
+ */
2083
+ async deleteJob(jobId) {
2084
+ const existing = await this.storage.getJob(jobId);
2085
+ if (!existing) {
2086
+ return false;
2087
+ }
2088
+ this.timerManager.untrackJob(jobId);
2089
+ const deleted = await this.storage.deleteJob(jobId);
2090
+ if (deleted) {
2091
+ await this.emitCronEvent(CronEvents.CRON_DELETED, existing);
2092
+ logger.info(`[CronService] Deleted job "${existing.name}" (${existing.id})`);
2093
+ }
2094
+ return deleted;
2095
+ }
2096
+ /**
2097
+ * Gets a job by ID
2098
+ * @param jobId The job ID
2099
+ * @returns The job or null if not found
2100
+ */
2101
+ async getJob(jobId) {
2102
+ return this.storage.getJob(jobId);
2103
+ }
2104
+ /**
2105
+ * Lists all jobs, optionally filtered
2106
+ * @param filter Optional filter criteria
2107
+ * @returns Array of matching jobs
2108
+ */
2109
+ async listJobs(filter) {
2110
+ return this.storage.listJobs(filter);
2111
+ }
2112
+ /**
2113
+ * Gets the count of jobs
2114
+ */
2115
+ async getJobCount() {
2116
+ return this.storage.getJobCount();
2117
+ }
2118
+ // ============================================================================
2119
+ // EXECUTION
2120
+ // ============================================================================
2121
+ /**
2122
+ * Manually runs a job immediately
2123
+ * @param jobId The job ID to run
2124
+ * @param mode 'force' to run even if disabled, 'due' to only run if due
2125
+ * @returns Execution result
2126
+ */
2127
+ async runJob(jobId, mode = "force") {
2128
+ const job = await this.storage.getJob(jobId);
2129
+ if (!job) {
2130
+ throw new Error(`Job not found: ${jobId}`);
2131
+ }
2132
+ if (mode === "due") {
2133
+ const nowMs = Date.now();
2134
+ const nextRunAtMs = job.state.nextRunAtMs;
2135
+ if (!nextRunAtMs || nowMs < nextRunAtMs) {
2136
+ return {
2137
+ ran: false,
2138
+ status: "skipped",
2139
+ durationMs: 0,
2140
+ error: "Job is not due yet"
2141
+ };
2142
+ }
2143
+ }
2144
+ if (!job.enabled && mode !== "force") {
2145
+ return {
2146
+ ran: false,
2147
+ status: "skipped",
2148
+ durationMs: 0,
2149
+ error: "Job is disabled"
2150
+ };
2151
+ }
2152
+ const result = await this.executeJobInternal(job);
2153
+ return {
2154
+ ran: true,
2155
+ ...result
2156
+ };
2157
+ }
2158
+ // ============================================================================
2159
+ // INTERNAL METHODS
2160
+ // ============================================================================
2161
+ /**
2162
+ * Handles a job becoming due (called by timer manager)
2163
+ */
2164
+ async handleJobDue(jobId) {
2165
+ const job = await this.storage.getJob(jobId);
2166
+ if (!job) {
2167
+ this.timerManager.untrackJob(jobId);
2168
+ return;
2169
+ }
2170
+ if (!job.enabled) {
2171
+ this.timerManager.untrackJob(jobId);
2172
+ return;
2173
+ }
2174
+ await this.executeJobInternal(job);
2175
+ }
2176
+ /**
2177
+ * Internal job execution with state management
2178
+ */
2179
+ async executeJobInternal(job) {
2180
+ const nowMs = Date.now();
2181
+ job.state.runningAtMs = nowMs;
2182
+ await this.storage.saveJob(job);
2183
+ logger.debug(`[CronService] Executing job "${job.name}" (${job.id})`);
2184
+ const result = await executeJob(this.runtime, job, this.cronConfig);
2185
+ job.state.runningAtMs = void 0;
2186
+ job.state.lastRunAtMs = nowMs;
2187
+ job.state.lastStatus = result.status;
2188
+ job.state.lastDurationMs = result.durationMs;
2189
+ if (result.status === "ok") {
2190
+ job.state.runCount += 1;
2191
+ job.state.lastError = void 0;
2192
+ } else {
2193
+ job.state.errorCount += 1;
2194
+ job.state.lastError = result.error;
2195
+ }
2196
+ const nextNowMs = Date.now();
2197
+ job.state.nextRunAtMs = computeNextRunAtMs(job.schedule, nextNowMs);
2198
+ job.updatedAtMs = nextNowMs;
2199
+ if (job.deleteAfterRun && result.status === "ok") {
2200
+ await this.storage.deleteJob(job.id);
2201
+ this.timerManager.untrackJob(job.id);
2202
+ logger.info(
2203
+ `[CronService] Deleted one-shot job "${job.name}" (${job.id}) after successful execution`
2204
+ );
2205
+ } else {
2206
+ await this.storage.saveJob(job);
2207
+ this.timerManager.markFinished(job.id, job);
2208
+ }
2209
+ const eventName = result.status === "ok" ? CronEvents.CRON_FIRED : CronEvents.CRON_FAILED;
2210
+ await this.emitCronEvent(eventName, job, result);
2211
+ logger.info(
2212
+ `[CronService] Job "${job.name}" (${job.id}) completed with status: ${result.status}` + (result.error ? ` - ${result.error}` : "")
2213
+ );
2214
+ return result;
2215
+ }
2216
+ /**
2217
+ * Handles catch-up for jobs that may have been missed while the service was stopped
2218
+ */
2219
+ async handleMissedJobs(jobs) {
2220
+ const nowMs = Date.now();
2221
+ const windowStart = nowMs - this.cronConfig.catchUpWindowMs;
2222
+ for (const job of jobs) {
2223
+ if (!job.enabled) {
2224
+ continue;
2225
+ }
2226
+ const lastRunAtMs = job.state.lastRunAtMs ?? 0;
2227
+ const nextRunAtMs = job.state.nextRunAtMs;
2228
+ if (nextRunAtMs && nextRunAtMs >= windowStart && nextRunAtMs < nowMs && lastRunAtMs < nextRunAtMs) {
2229
+ logger.info(
2230
+ `[CronService] Catching up missed job "${job.name}" (${job.id}) that was due at ${new Date(nextRunAtMs).toISOString()}`
2231
+ );
2232
+ await this.executeJobInternal(job);
2233
+ }
2234
+ }
2235
+ }
2236
+ /**
2237
+ * Emits a cron event
2238
+ */
2239
+ async emitCronEvent(eventName, job, result) {
2240
+ const eventData = {
2241
+ jobId: job.id,
2242
+ jobName: job.name,
2243
+ schedule: job.schedule
2244
+ };
2245
+ if (result) {
2246
+ eventData.result = {
2247
+ status: result.status,
2248
+ durationMs: result.durationMs,
2249
+ output: result.output,
2250
+ error: result.error
2251
+ };
2252
+ }
2253
+ await this.runtime.emitEvent(eventName, {
2254
+ runtime: this.runtime,
2255
+ source: `cron:${job.id}`,
2256
+ ...eventData
2257
+ });
2258
+ }
2259
+ // ============================================================================
2260
+ // STATUS AND DIAGNOSTICS
2261
+ // ============================================================================
2262
+ /**
2263
+ * Gets the service status
2264
+ */
2265
+ async getStatus() {
2266
+ return {
2267
+ initialized: this.initialized,
2268
+ jobCount: await this.storage.getJobCount(),
2269
+ trackedJobCount: this.timerManager?.getTrackedJobCount() ?? 0,
2270
+ config: this.cronConfig
2271
+ };
2272
+ }
2273
+ /**
2274
+ * Performs a health check
2275
+ */
2276
+ async healthCheck() {
2277
+ const issues = [];
2278
+ if (!this.initialized) {
2279
+ issues.push("Service not initialized");
2280
+ }
2281
+ if (!this.storage) {
2282
+ issues.push("Storage not available");
2283
+ }
2284
+ if (!this.timerManager) {
2285
+ issues.push("Timer manager not available");
2286
+ }
2287
+ return {
2288
+ healthy: issues.length === 0,
2289
+ issues
2290
+ };
2291
+ }
2292
+ };
2293
+
2294
+ // src/cli/index.ts
2295
+ import { defineCliCommand, registerCliCommand } from "@elizaos/plugin-cli";
2296
+
2297
+ // src/cli/register.ts
2298
+ var defaultLogger = {
2299
+ info: (msg) => console.log(msg),
2300
+ warn: (msg) => console.warn(msg),
2301
+ error: (msg) => console.error(msg),
2302
+ debug: (msg) => {
2303
+ if (process.env.DEBUG) console.debug(msg);
2304
+ }
2305
+ };
2306
+ function getLogger(ctx) {
2307
+ return ctx.logger ?? defaultLogger;
2308
+ }
2309
+ function getCronService2(ctx) {
2310
+ const logger2 = getLogger(ctx);
2311
+ const runtime = ctx.getRuntime?.();
2312
+ if (!runtime) {
2313
+ logger2.error("No runtime available");
2314
+ return null;
2315
+ }
2316
+ const service = runtime.getService(CRON_SERVICE_TYPE);
2317
+ if (!service) {
2318
+ logger2.error("CronService not available. Is the cron plugin enabled?");
2319
+ return null;
2320
+ }
2321
+ return service;
2322
+ }
2323
+ function registerCronCli(ctx) {
2324
+ const logger2 = getLogger(ctx);
2325
+ const cron = ctx.program.command("cron").description("Manage cron jobs");
2326
+ cron.command("status").description("Show cron scheduler status").option("--json", "Output JSON", false).action(async (opts) => {
2327
+ const service = getCronService2(ctx);
2328
+ if (!service) {
2329
+ process.exitCode = 1;
2330
+ return;
2331
+ }
2332
+ try {
2333
+ const status = await service.getStatus();
2334
+ if (opts.json) {
2335
+ logger2.info(JSON.stringify(status, null, 2));
2336
+ } else {
2337
+ logger2.info(`Cron Service Status:`);
2338
+ logger2.info(` Initialized: ${status.initialized}`);
2339
+ logger2.info(` Jobs: ${status.jobCount}`);
2340
+ logger2.info(` Tracked: ${status.trackedJobCount}`);
2341
+ }
2342
+ } catch (err) {
2343
+ logger2.error(`Failed to get status: ${err}`);
2344
+ process.exitCode = 1;
2345
+ }
2346
+ });
2347
+ cron.command("list").description("List cron jobs").option("--all", "Include disabled jobs", false).option("--json", "Output JSON", false).action(async (opts) => {
2348
+ const service = getCronService2(ctx);
2349
+ if (!service) {
2350
+ process.exitCode = 1;
2351
+ return;
2352
+ }
2353
+ try {
2354
+ const jobs = await service.listJobs({
2355
+ includeDisabled: opts.all
2356
+ });
2357
+ if (opts.json) {
2358
+ logger2.info(JSON.stringify({ jobs }, null, 2));
2359
+ } else {
2360
+ printCronList(jobs, logger2.info);
2361
+ }
2362
+ } catch (err) {
2363
+ logger2.error(`Failed to list jobs: ${err}`);
2364
+ process.exitCode = 1;
2365
+ }
2366
+ });
2367
+ cron.command("add").alias("create").description("Add a cron job").requiredOption("--name <name>", "Job name").option("--description <text>", "Optional description").option("--disabled", "Create job disabled", false).option("--delete-after-run", "Delete one-shot job after it succeeds", false).option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)").option("--every <duration>", "Run every duration (e.g. 10m, 1h)").option("--cron <expr>", "Cron expression (5-field)").option("--tz <iana>", "Timezone for cron expressions (IANA)").option("--prompt <text>", "Prompt to execute").option("--action <name>", "Action to execute").option("--event <name>", "Event to emit").option("--session <target>", "Session target: main or isolated").option("--system-event <text>", "System event text (main session)").option("--message <text>", "Agent message (isolated session)").option("--wake <mode>", "Wake mode: now or next-heartbeat", "next-heartbeat").option("--announce", "Enable announce delivery for isolated jobs", false).option("--channel <name>", "Delivery channel (e.g. whatsapp, telegram, discord, last)").option("--to <target>", "Delivery target (e.g. phone number, channel ID)").option("--agent <id>", "Agent ID to bind this job to").option("--json", "Output JSON", false).action(async (opts) => {
2368
+ const service = getCronService2(ctx);
2369
+ if (!service) {
2370
+ process.exitCode = 1;
2371
+ return;
2372
+ }
2373
+ const isOttoStyle = Boolean(opts.session || opts.systemEvent || opts.message);
2374
+ const schedule = parseScheduleOpts(opts);
2375
+ if (!schedule) {
2376
+ logger2.error("Choose exactly one schedule: --at, --every, or --cron");
2377
+ process.exitCode = 1;
2378
+ return;
2379
+ }
2380
+ if (isOttoStyle) {
2381
+ const ottoInput = buildOttoJobInput(opts, schedule, logger2);
2382
+ if (!ottoInput) {
2383
+ process.exitCode = 1;
2384
+ return;
2385
+ }
2386
+ const job = await service.createJob(ottoInput);
2387
+ if (opts.json) {
2388
+ logger2.info(JSON.stringify(job, null, 2));
2389
+ } else {
2390
+ logger2.info(`Created job: ${job.id}`);
2391
+ logger2.info(` Name: ${ottoInput.name}`);
2392
+ logger2.info(
2393
+ ` Session: ${ottoInput.sessionTarget ?? "n/a"}`
2394
+ );
2395
+ logger2.info(
2396
+ ` Wake: ${ottoInput.wakeMode ?? "next-heartbeat"}`
2397
+ );
2398
+ logger2.info(
2399
+ ` Next run: ${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "N/A"}`
2400
+ );
2401
+ }
2402
+ } else {
2403
+ const payload = parsePayloadOpts(opts);
2404
+ if (!payload) {
2405
+ logger2.error(
2406
+ "Choose exactly one payload: --prompt, --action, --event, --system-event, or --message"
2407
+ );
2408
+ process.exitCode = 1;
2409
+ return;
2410
+ }
2411
+ const input = {
2412
+ name: opts.name,
2413
+ description: opts.description,
2414
+ enabled: !opts.disabled,
2415
+ deleteAfterRun: opts.deleteAfterRun || void 0,
2416
+ schedule,
2417
+ payload
2418
+ };
2419
+ const job = await service.createJob(input);
2420
+ if (opts.json) {
2421
+ logger2.info(JSON.stringify(job, null, 2));
2422
+ } else {
2423
+ logger2.info(`Created job: ${job.id}`);
2424
+ logger2.info(` Name: ${job.name}`);
2425
+ logger2.info(` Enabled: ${job.enabled}`);
2426
+ logger2.info(
2427
+ ` Next run: ${job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString() : "N/A"}`
2428
+ );
2429
+ }
2430
+ }
2431
+ });
2432
+ cron.command("edit").description("Edit a cron job").argument("<id>", "Job ID").option("--name <name>", "Set name").option("--description <text>", "Set description").option("--enable", "Enable job").option("--disable", "Disable job").option("--at <when>", "Set one-shot time").option("--every <duration>", "Set interval").option("--cron <expr>", "Set cron expression").option("--tz <iana>", "Set timezone").option("--json", "Output JSON", false).action(async (id, opts) => {
2433
+ const service = getCronService2(ctx);
2434
+ if (!service) {
2435
+ process.exitCode = 1;
2436
+ return;
2437
+ }
2438
+ try {
2439
+ const patch = {};
2440
+ if (opts.name) patch.name = opts.name;
2441
+ if (opts.description) patch.description = opts.description;
2442
+ if (opts.enable) patch.enabled = true;
2443
+ if (opts.disable) patch.enabled = false;
2444
+ const schedule = parseScheduleOpts(opts);
2445
+ if (schedule) {
2446
+ patch.schedule = schedule;
2447
+ }
2448
+ const job = await service.updateJob(id, patch);
2449
+ if (opts.json) {
2450
+ logger2.info(JSON.stringify(job, null, 2));
2451
+ } else {
2452
+ logger2.info(`Updated job: ${job.id}`);
2453
+ }
2454
+ } catch (err) {
2455
+ logger2.error(`Failed to update job: ${err}`);
2456
+ process.exitCode = 1;
2457
+ }
2458
+ });
2459
+ cron.command("rm").alias("remove").alias("delete").description("Remove a cron job").argument("<id>", "Job ID").option("--json", "Output JSON", false).action(async (id, opts) => {
2460
+ const service = getCronService2(ctx);
2461
+ if (!service) {
2462
+ process.exitCode = 1;
2463
+ return;
2464
+ }
2465
+ try {
2466
+ const deleted = await service.deleteJob(id);
2467
+ if (opts.json) {
2468
+ logger2.info(JSON.stringify({ deleted }, null, 2));
2469
+ } else {
2470
+ logger2.info(deleted ? `Deleted job: ${id}` : `Job not found: ${id}`);
2471
+ }
2472
+ } catch (err) {
2473
+ logger2.error(`Failed to delete job: ${err}`);
2474
+ process.exitCode = 1;
2475
+ }
2476
+ });
2477
+ cron.command("enable").description("Enable a cron job").argument("<id>", "Job ID").action(async (id) => {
2478
+ const service = getCronService2(ctx);
2479
+ if (!service) {
2480
+ process.exitCode = 1;
2481
+ return;
2482
+ }
2483
+ try {
2484
+ await service.updateJob(id, { enabled: true });
2485
+ logger2.info(`Enabled job: ${id}`);
2486
+ } catch (err) {
2487
+ logger2.error(`Failed to enable job: ${err}`);
2488
+ process.exitCode = 1;
2489
+ }
2490
+ });
2491
+ cron.command("disable").description("Disable a cron job").argument("<id>", "Job ID").action(async (id) => {
2492
+ const service = getCronService2(ctx);
2493
+ if (!service) {
2494
+ process.exitCode = 1;
2495
+ return;
2496
+ }
2497
+ try {
2498
+ await service.updateJob(id, { enabled: false });
2499
+ logger2.info(`Disabled job: ${id}`);
2500
+ } catch (err) {
2501
+ logger2.error(`Failed to disable job: ${err}`);
2502
+ process.exitCode = 1;
2503
+ }
2504
+ });
2505
+ cron.command("run").description("Run a cron job now").argument("<id>", "Job ID").option("--force", "Run even if not due", false).action(async (id, opts) => {
2506
+ const service = getCronService2(ctx);
2507
+ if (!service) {
2508
+ process.exitCode = 1;
2509
+ return;
2510
+ }
2511
+ try {
2512
+ const result = await service.runJob(id, opts.force ? "force" : "due");
2513
+ logger2.info(JSON.stringify(result, null, 2));
2514
+ } catch (err) {
2515
+ logger2.error(`Failed to run job: ${err}`);
2516
+ process.exitCode = 1;
2517
+ }
2518
+ });
2519
+ }
2520
+ function parseScheduleOpts(opts) {
2521
+ const at = typeof opts.at === "string" ? opts.at : "";
2522
+ const every = typeof opts.every === "string" ? opts.every : "";
2523
+ const cronExpr = typeof opts.cron === "string" ? opts.cron : "";
2524
+ const chosen = [Boolean(at), Boolean(every), Boolean(cronExpr)].filter(Boolean).length;
2525
+ if (chosen === 0) return null;
2526
+ if (chosen > 1) return null;
2527
+ if (at) {
2528
+ const atIso = parseAt(at);
2529
+ if (!atIso) return null;
2530
+ return { kind: "at", at: atIso };
2531
+ }
2532
+ if (every) {
2533
+ const everyMs = parseDurationMs(every);
2534
+ if (!everyMs) return null;
2535
+ return { kind: "every", everyMs };
2536
+ }
2537
+ if (cronExpr) {
2538
+ const tz = typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : void 0;
2539
+ return { kind: "cron", expr: cronExpr, tz };
2540
+ }
2541
+ return null;
2542
+ }
2543
+ function parsePayloadOpts(opts) {
2544
+ const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
2545
+ const action = typeof opts.action === "string" ? opts.action : "";
2546
+ const event = typeof opts.event === "string" ? opts.event : "";
2547
+ const chosen = [Boolean(prompt), Boolean(action), Boolean(event)].filter(Boolean).length;
2548
+ if (chosen === 0) return null;
2549
+ if (chosen > 1) return null;
2550
+ if (prompt) {
2551
+ return { kind: "prompt", text: prompt };
2552
+ }
2553
+ if (action) {
2554
+ return { kind: "action", actionName: action };
2555
+ }
2556
+ if (event) {
2557
+ return { kind: "event", eventName: event };
2558
+ }
2559
+ return null;
2560
+ }
2561
+ function buildOttoJobInput(opts, schedule, cliLogger) {
2562
+ const systemEvent = typeof opts.systemEvent === "string" ? opts.systemEvent.trim() : "";
2563
+ const message = typeof opts.message === "string" ? opts.message.trim() : "";
2564
+ let sessionTarget;
2565
+ if (typeof opts.session === "string") {
2566
+ sessionTarget = opts.session.trim().toLowerCase();
2567
+ } else if (systemEvent) {
2568
+ sessionTarget = "main";
2569
+ } else if (message) {
2570
+ sessionTarget = "isolated";
2571
+ } else {
2572
+ cliLogger.error("Provide --system-event (main) or --message (isolated)");
2573
+ return null;
2574
+ }
2575
+ if (sessionTarget !== "main" && sessionTarget !== "isolated") {
2576
+ cliLogger.error('--session must be "main" or "isolated"');
2577
+ return null;
2578
+ }
2579
+ let payload;
2580
+ if (sessionTarget === "main") {
2581
+ if (!systemEvent) {
2582
+ cliLogger.error("Main session jobs require --system-event <text>");
2583
+ return null;
2584
+ }
2585
+ payload = { kind: "systemEvent", text: systemEvent };
2586
+ } else {
2587
+ if (!message) {
2588
+ cliLogger.error("Isolated session jobs require --message <text>");
2589
+ return null;
2590
+ }
2591
+ payload = { kind: "agentTurn", message };
2592
+ }
2593
+ let delivery;
2594
+ if (sessionTarget === "isolated" && (opts.announce || opts.channel || opts.to)) {
2595
+ delivery = {
2596
+ mode: opts.announce ? "announce" : "none",
2597
+ channel: typeof opts.channel === "string" ? opts.channel.trim() : "last",
2598
+ to: typeof opts.to === "string" ? opts.to.trim() : void 0
2599
+ };
2600
+ }
2601
+ const wakeMode = typeof opts.wake === "string" && opts.wake.trim() === "now" ? "now" : "next-heartbeat";
2602
+ return {
2603
+ name: typeof opts.name === "string" ? opts.name : "",
2604
+ description: typeof opts.description === "string" ? opts.description : void 0,
2605
+ enabled: !opts.disabled,
2606
+ deleteAfterRun: Boolean(opts.deleteAfterRun) || (schedule.kind === "at" ? true : void 0),
2607
+ schedule,
2608
+ sessionTarget,
2609
+ wakeMode,
2610
+ payload,
2611
+ delivery,
2612
+ agentId: typeof opts.agent === "string" && opts.agent.trim() ? opts.agent.trim() : void 0
2613
+ };
2614
+ }
2615
+
2616
+ // src/cli/index.ts
2617
+ registerCliCommand(
2618
+ defineCliCommand("cron", "Cron job scheduling commands", (ctx) => registerCronCli(ctx), {
2619
+ priority: 50
2620
+ })
2621
+ );
2622
+ function parseDurationMs(input) {
2623
+ const raw = input.trim();
2624
+ if (!raw) return null;
2625
+ const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
2626
+ if (!match) return null;
2627
+ const n = Number.parseFloat(match[1] ?? "");
2628
+ if (!Number.isFinite(n) || n <= 0) return null;
2629
+ const unit = (match[2] ?? "").toLowerCase();
2630
+ const factor = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2631
+ return Math.floor(n * factor);
2632
+ }
2633
+ function parseAt(input) {
2634
+ const raw = input.trim();
2635
+ if (!raw) return null;
2636
+ const date = new Date(raw);
2637
+ if (!Number.isNaN(date.getTime())) {
2638
+ return date.toISOString();
2639
+ }
2640
+ const dur = parseDurationMs(raw);
2641
+ if (dur !== null) {
2642
+ return new Date(Date.now() + dur).toISOString();
2643
+ }
2644
+ return null;
2645
+ }
2646
+ function printCronList(jobs, log) {
2647
+ if (jobs.length === 0) {
2648
+ log("No cron jobs.");
2649
+ return;
2650
+ }
2651
+ log(
2652
+ "ID | Name | Status | Schedule | Next Run"
2653
+ );
2654
+ log(
2655
+ "-------------------------------------|--------------------------|----------|----------------------------------|--------------------"
2656
+ );
2657
+ for (const job of jobs) {
2658
+ const id = job.id.padEnd(36);
2659
+ const name = (job.name || "(unnamed)").slice(0, 24).padEnd(24);
2660
+ const status = (job.enabled ? "enabled" : "disabled").padEnd(8);
2661
+ const schedule = formatSchedule(job.schedule).slice(0, 32).padEnd(32);
2662
+ const next = job.state.nextRunAtMs ? new Date(job.state.nextRunAtMs).toISOString().slice(0, 19) : "N/A";
2663
+ log(`${id} | ${name} | ${status} | ${schedule} | ${next}`);
2664
+ }
2665
+ }
2666
+
2667
+ // src/index.ts
2668
+ var CronPluginEvents = {
2669
+ /** Request an immediate heartbeat tick. Payload: { text?: string } */
2670
+ HEARTBEAT_WAKE: "HEARTBEAT_WAKE",
2671
+ /** Enqueue a system event for the next heartbeat. Payload: { text: string } */
2672
+ HEARTBEAT_SYSTEM_EVENT: "HEARTBEAT_SYSTEM_EVENT"
2673
+ };
2674
+ var cronPlugin = {
2675
+ name: "cron",
2676
+ description: "Cron job scheduling for recurring and one-time task automation",
2677
+ // Register the cron service
2678
+ services: [CronService],
2679
+ // Actions for cron operations
2680
+ actions: [createCronAction, updateCronAction, deleteCronAction, listCronsAction, runCronAction],
2681
+ // Provider for cron context
2682
+ providers: [cronContextProvider],
2683
+ // HTTP routes for UI
2684
+ routes: cronRoutes,
2685
+ // Event handlers for cross-plugin coordination
2686
+ events: {
2687
+ [CronPluginEvents.HEARTBEAT_WAKE]: [
2688
+ async (payload) => {
2689
+ const runtime = payload.runtime;
2690
+ if (!runtime) {
2691
+ return;
2692
+ }
2693
+ const text = typeof payload.text === "string" ? payload.text : void 0;
2694
+ if (text) {
2695
+ pushSystemEvent(
2696
+ runtime.agentId,
2697
+ text,
2698
+ typeof payload.source === "string" ? payload.source : "external"
2699
+ );
2700
+ }
2701
+ await wakeHeartbeatNow(runtime);
2702
+ }
2703
+ ],
2704
+ [CronPluginEvents.HEARTBEAT_SYSTEM_EVENT]: [
2705
+ async (payload) => {
2706
+ const runtime = payload.runtime;
2707
+ if (!runtime) {
2708
+ return;
2709
+ }
2710
+ const text = typeof payload.text === "string" ? payload.text : "";
2711
+ if (text) {
2712
+ pushSystemEvent(
2713
+ runtime.agentId,
2714
+ text,
2715
+ typeof payload.source === "string" ? payload.source : "external"
2716
+ );
2717
+ }
2718
+ }
2719
+ ]
2720
+ }
2721
+ };
2722
+ var index_default = cronPlugin;
2723
+ export {
2724
+ CRON_JOB_COMPONENT_PREFIX,
2725
+ CRON_JOB_INDEX_COMPONENT,
2726
+ CRON_SERVICE_TYPE,
2727
+ CronActions,
2728
+ CronEvents,
2729
+ CronPluginEvents,
2730
+ CronService,
2731
+ DEFAULT_CRON_CONFIG,
2732
+ HEARTBEAT_WORKER_NAME,
2733
+ TimerManager,
2734
+ computeNextRunAtMs,
2735
+ cronPlugin,
2736
+ index_default as default,
2737
+ drainSystemEvents,
2738
+ executeJob,
2739
+ formatSchedule,
2740
+ getCronStorage,
2741
+ heartbeatWorker,
2742
+ isWithinActiveHours,
2743
+ otto_exports as otto,
2744
+ parseDuration,
2745
+ parseScheduleDescription,
2746
+ pendingEventCount,
2747
+ pushSystemEvent,
2748
+ resolveHeartbeatConfig,
2749
+ startHeartbeat,
2750
+ validateJobExecutability,
2751
+ validateSchedule,
2752
+ wakeHeartbeatNow
2753
+ };