@elizaos/plugin-cron 2.0.0-alpha.5 → 2.0.0-alpha.7

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