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