@sentry/junior-scheduler 0.107.0 → 0.108.0
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/README.md +10 -1
- package/dist/cadence.d.ts +12 -10
- package/dist/index.js +557 -421
- package/dist/schedule-intent.d.ts +61 -0
- package/dist/store.d.ts +1 -0
- package/dist/tool-support.d.ts +8 -31
- package/dist/tools/create-task.d.ts +23 -5
- package/dist/tools/update-task.d.ts +23 -4
- package/package.json +2 -2
- package/src/cadence.ts +228 -304
- package/src/schedule-intent.ts +372 -0
- package/src/store.ts +35 -12
- package/src/tool-support.ts +25 -143
- package/src/tools/create-task.ts +66 -69
- package/src/tools/update-task.ts +59 -71
package/dist/index.js
CHANGED
|
@@ -25,26 +25,6 @@ import {
|
|
|
25
25
|
import { z as z2 } from "zod";
|
|
26
26
|
|
|
27
27
|
// src/cadence.ts
|
|
28
|
-
function parseScheduleTimestamp(value) {
|
|
29
|
-
const trimmed = value.trim();
|
|
30
|
-
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/.exec(
|
|
31
|
-
trimmed
|
|
32
|
-
);
|
|
33
|
-
if (!match) {
|
|
34
|
-
return void 0;
|
|
35
|
-
}
|
|
36
|
-
const year = Number(match[1]);
|
|
37
|
-
const month = Number(match[2]);
|
|
38
|
-
const day = Number(match[3]);
|
|
39
|
-
const hour = Number(match[4]);
|
|
40
|
-
const minute = Number(match[5]);
|
|
41
|
-
const second = match[6] ? Number(match[6]) : 0;
|
|
42
|
-
if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || !Number.isInteger(hour) || !Number.isInteger(minute) || !Number.isInteger(second) || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month) || hour < 0 || hour > 23 || minute < 0 || minute > 59 || second < 0 || second > 59) {
|
|
43
|
-
return void 0;
|
|
44
|
-
}
|
|
45
|
-
const parsed = Date.parse(trimmed);
|
|
46
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
47
|
-
}
|
|
48
28
|
var FORMATTERS = /* @__PURE__ */ new Map();
|
|
49
29
|
function getFormatter(timezone) {
|
|
50
30
|
const existing = FORMATTERS.get(timezone);
|
|
@@ -70,6 +50,9 @@ function normalizeHour(hour) {
|
|
|
70
50
|
function getLocalDateWeekday(date) {
|
|
71
51
|
return new Date(Date.UTC(date.year, date.month - 1, date.day)).getUTCDay();
|
|
72
52
|
}
|
|
53
|
+
function getWeekStart(date) {
|
|
54
|
+
return addDays(date, -((getLocalDateWeekday(date) + 6) % 7));
|
|
55
|
+
}
|
|
73
56
|
function getZonedDateTimeParts(timestampMs, timezone) {
|
|
74
57
|
const parts = getFormatter(timezone).formatToParts(new Date(timestampMs));
|
|
75
58
|
const values = new Map(parts.map((part) => [part.type, part.value]));
|
|
@@ -109,15 +92,163 @@ function localDateTimeToTimestampMs(args) {
|
|
|
109
92
|
args.time.minute,
|
|
110
93
|
0
|
|
111
94
|
);
|
|
112
|
-
|
|
113
|
-
for (
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
95
|
+
const offsets = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const probeDeltaMs of [
|
|
97
|
+
-36 * 60 * 60 * 1e3,
|
|
98
|
+
-12 * 60 * 60 * 1e3,
|
|
99
|
+
0,
|
|
100
|
+
12 * 60 * 60 * 1e3,
|
|
101
|
+
36 * 60 * 60 * 1e3
|
|
102
|
+
]) {
|
|
103
|
+
offsets.add(
|
|
104
|
+
getTimeZoneOffsetMs(localAsUtcMs + probeDeltaMs, args.timezone)
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const matches = [...offsets].map((offsetMs) => localAsUtcMs - offsetMs).filter((timestampMs) => {
|
|
108
|
+
const parts = getZonedDateTimeParts(timestampMs, args.timezone);
|
|
109
|
+
return parts.year === args.date.year && parts.month === args.date.month && parts.day === args.date.day && parts.hour === args.time.hour && parts.minute === args.time.minute;
|
|
110
|
+
});
|
|
111
|
+
if (matches.length === 0) {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
return Math.min(...matches);
|
|
115
|
+
}
|
|
116
|
+
function resolveLocalScheduleAtMs(args) {
|
|
117
|
+
const date = parseLocalDate(args.date);
|
|
118
|
+
if (!date) {
|
|
119
|
+
return void 0;
|
|
120
|
+
}
|
|
121
|
+
return localDateTimeToTimestampMs({
|
|
122
|
+
date,
|
|
123
|
+
time: args.time,
|
|
124
|
+
timezone: args.timezone
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function daysBetween(left, right) {
|
|
128
|
+
return Math.floor(
|
|
129
|
+
(Date.UTC(right.year, right.month - 1, right.day) - Date.UTC(left.year, left.month - 1, left.day)) / (24 * 60 * 60 * 1e3)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function weeklyRecurrenceMatchesDate(date, start, recurrence) {
|
|
133
|
+
if (compareDate(date, start) < 0) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return normalizeWeekdays(recurrence.weekdays).includes(
|
|
137
|
+
getLocalDateWeekday(date)
|
|
138
|
+
) && Math.floor(daysBetween(getWeekStart(start), getWeekStart(date)) / 7) % recurrence.interval === 0;
|
|
139
|
+
}
|
|
140
|
+
function greatestCommonDivisor(left, right) {
|
|
141
|
+
let a = Math.abs(left);
|
|
142
|
+
let b = Math.abs(right);
|
|
143
|
+
while (b !== 0) {
|
|
144
|
+
[a, b] = [b, a % b];
|
|
145
|
+
}
|
|
146
|
+
return a;
|
|
147
|
+
}
|
|
148
|
+
function findNextRunAtMs(args) {
|
|
149
|
+
const start = parseLocalDate(args.recurrence.startDate);
|
|
150
|
+
const interval = args.recurrence.interval;
|
|
151
|
+
if (!start || !Number.isInteger(interval) || interval <= 0) {
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
const searchFrom = compareDate(args.searchFrom, start) < 0 ? start : args.searchFrom;
|
|
155
|
+
if (args.recurrence.frequency === "daily") {
|
|
156
|
+
const offsetDays = daysBetween(start, searchFrom);
|
|
157
|
+
let candidateDate = addDays(
|
|
158
|
+
start,
|
|
159
|
+
Math.ceil(offsetDays / interval) * interval
|
|
160
|
+
);
|
|
161
|
+
const gregorianCycleDays = 146097;
|
|
162
|
+
const candidateCount2 = gregorianCycleDays / greatestCommonDivisor(gregorianCycleDays, interval);
|
|
163
|
+
for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
|
|
164
|
+
const candidate = buildCandidate({
|
|
165
|
+
date: candidateDate,
|
|
166
|
+
recurrence: args.recurrence,
|
|
167
|
+
timezone: args.timezone
|
|
168
|
+
});
|
|
169
|
+
if (candidate !== void 0 && candidate > args.afterMs) {
|
|
170
|
+
return candidate;
|
|
171
|
+
}
|
|
172
|
+
candidateDate = addDays(candidateDate, interval);
|
|
117
173
|
}
|
|
118
|
-
|
|
174
|
+
return void 0;
|
|
119
175
|
}
|
|
120
|
-
|
|
176
|
+
if (args.recurrence.frequency === "weekly") {
|
|
177
|
+
if (normalizeWeekdays(args.recurrence.weekdays).length === 0) {
|
|
178
|
+
return void 0;
|
|
179
|
+
}
|
|
180
|
+
let candidateDate = searchFrom;
|
|
181
|
+
const gregorianCycleDays = 146097;
|
|
182
|
+
const cadenceDays = interval * 7;
|
|
183
|
+
const searchDays = gregorianCycleDays * cadenceDays / greatestCommonDivisor(gregorianCycleDays, cadenceDays);
|
|
184
|
+
for (let attempts = 0; attempts < searchDays; attempts += 1) {
|
|
185
|
+
if (weeklyRecurrenceMatchesDate(candidateDate, start, args.recurrence)) {
|
|
186
|
+
const candidate = buildCandidate({
|
|
187
|
+
date: candidateDate,
|
|
188
|
+
recurrence: args.recurrence,
|
|
189
|
+
timezone: args.timezone
|
|
190
|
+
});
|
|
191
|
+
if (candidate !== void 0 && candidate > args.afterMs) {
|
|
192
|
+
return candidate;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
candidateDate = addDays(candidateDate, 1);
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
if (args.recurrence.frequency === "monthly") {
|
|
200
|
+
const startMonth = start.year * 12 + start.month - 1;
|
|
201
|
+
const searchMonth = searchFrom.year * 12 + searchFrom.month - 1;
|
|
202
|
+
let candidateMonth = startMonth + Math.ceil((searchMonth - startMonth) / interval) * interval;
|
|
203
|
+
const gregorianCycleMonths = 4800;
|
|
204
|
+
const candidateCount2 = gregorianCycleMonths / greatestCommonDivisor(gregorianCycleMonths, interval);
|
|
205
|
+
for (let attempts = 0; attempts < candidateCount2; attempts += 1) {
|
|
206
|
+
const candidateDate = {
|
|
207
|
+
year: Math.floor(candidateMonth / 12),
|
|
208
|
+
month: candidateMonth % 12 + 1,
|
|
209
|
+
day: args.recurrence.dayOfMonth ?? 0
|
|
210
|
+
};
|
|
211
|
+
if (compareDate(candidateDate, searchFrom) >= 0) {
|
|
212
|
+
const candidate = buildCandidate({
|
|
213
|
+
date: candidateDate,
|
|
214
|
+
recurrence: args.recurrence,
|
|
215
|
+
timezone: args.timezone
|
|
216
|
+
});
|
|
217
|
+
if (candidate !== void 0 && candidate > args.afterMs) {
|
|
218
|
+
return candidate;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
candidateMonth += interval;
|
|
222
|
+
}
|
|
223
|
+
return void 0;
|
|
224
|
+
}
|
|
225
|
+
let candidateYear = start.year + Math.ceil((searchFrom.year - start.year) / interval) * interval;
|
|
226
|
+
const candidateCount = 400 / greatestCommonDivisor(400, interval);
|
|
227
|
+
for (let attempts = 0; attempts < candidateCount; attempts += 1) {
|
|
228
|
+
const candidateDate = {
|
|
229
|
+
year: candidateYear,
|
|
230
|
+
month: args.recurrence.month ?? 0,
|
|
231
|
+
day: args.recurrence.dayOfMonth ?? 0
|
|
232
|
+
};
|
|
233
|
+
if (compareDate(candidateDate, searchFrom) >= 0) {
|
|
234
|
+
const candidate = buildCandidate({
|
|
235
|
+
date: candidateDate,
|
|
236
|
+
recurrence: args.recurrence,
|
|
237
|
+
timezone: args.timezone
|
|
238
|
+
});
|
|
239
|
+
if (candidate !== void 0 && candidate > args.afterMs) {
|
|
240
|
+
return candidate;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
candidateYear += interval;
|
|
244
|
+
}
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
function getFirstRunAtMs(args) {
|
|
248
|
+
return findNextRunAtMs({
|
|
249
|
+
...args,
|
|
250
|
+
searchFrom: getLocalDate(args.afterMs, args.timezone)
|
|
251
|
+
});
|
|
121
252
|
}
|
|
122
253
|
function compareDate(left, right) {
|
|
123
254
|
return Date.UTC(left.year, left.month - 1, left.day) - Date.UTC(right.year, right.month - 1, right.day);
|
|
@@ -146,13 +277,6 @@ function parseLocalDate(value) {
|
|
|
146
277
|
}
|
|
147
278
|
return { year, month, day };
|
|
148
279
|
}
|
|
149
|
-
function formatLocalDate(date) {
|
|
150
|
-
return [
|
|
151
|
-
String(date.year).padStart(4, "0"),
|
|
152
|
-
String(date.month).padStart(2, "0"),
|
|
153
|
-
String(date.day).padStart(2, "0")
|
|
154
|
-
].join("-");
|
|
155
|
-
}
|
|
156
280
|
function getLocalDate(timestampMs, timezone) {
|
|
157
281
|
const parts = getZonedDateTimeParts(timestampMs, timezone);
|
|
158
282
|
return { year: parts.year, month: parts.month, day: parts.day };
|
|
@@ -169,168 +293,6 @@ function buildCandidate(args) {
|
|
|
169
293
|
timezone: args.timezone
|
|
170
294
|
});
|
|
171
295
|
}
|
|
172
|
-
function getDailyNextRunAtMs(args) {
|
|
173
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
174
|
-
if (!start) {
|
|
175
|
-
return void 0;
|
|
176
|
-
}
|
|
177
|
-
let candidateDate = addDays(
|
|
178
|
-
getLocalDate(args.scheduledForMs, args.timezone),
|
|
179
|
-
args.recurrence.interval
|
|
180
|
-
);
|
|
181
|
-
if (compareDate(candidateDate, start) < 0) {
|
|
182
|
-
candidateDate = start;
|
|
183
|
-
}
|
|
184
|
-
let candidate = buildCandidate({
|
|
185
|
-
date: candidateDate,
|
|
186
|
-
recurrence: args.recurrence,
|
|
187
|
-
timezone: args.timezone
|
|
188
|
-
});
|
|
189
|
-
while (candidate <= args.afterMs) {
|
|
190
|
-
candidateDate = addDays(candidateDate, args.recurrence.interval);
|
|
191
|
-
candidate = buildCandidate({
|
|
192
|
-
date: candidateDate,
|
|
193
|
-
recurrence: args.recurrence,
|
|
194
|
-
timezone: args.timezone
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
return candidate;
|
|
198
|
-
}
|
|
199
|
-
function getWeeklyNextRunAtMs(args) {
|
|
200
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
201
|
-
if (!start) {
|
|
202
|
-
return void 0;
|
|
203
|
-
}
|
|
204
|
-
const weekdays = normalizeWeekdays(args.recurrence.weekdays);
|
|
205
|
-
if (weekdays.length === 0) {
|
|
206
|
-
return void 0;
|
|
207
|
-
}
|
|
208
|
-
let candidateDate = addDays(
|
|
209
|
-
getLocalDate(args.scheduledForMs, args.timezone),
|
|
210
|
-
1
|
|
211
|
-
);
|
|
212
|
-
for (let attempts = 0; attempts < 3660; attempts += 1) {
|
|
213
|
-
const weeksSinceStart = Math.floor(
|
|
214
|
-
(Date.UTC(
|
|
215
|
-
candidateDate.year,
|
|
216
|
-
candidateDate.month - 1,
|
|
217
|
-
candidateDate.day
|
|
218
|
-
) - Date.UTC(start.year, start.month - 1, start.day)) / (7 * 24 * 60 * 60 * 1e3)
|
|
219
|
-
);
|
|
220
|
-
const isInCycle = weeksSinceStart >= 0 && weeksSinceStart % args.recurrence.interval === 0;
|
|
221
|
-
if (isInCycle && weekdays.includes(getLocalDateWeekday(candidateDate))) {
|
|
222
|
-
const candidate = buildCandidate({
|
|
223
|
-
date: candidateDate,
|
|
224
|
-
recurrence: args.recurrence,
|
|
225
|
-
timezone: args.timezone
|
|
226
|
-
});
|
|
227
|
-
if (candidate > args.afterMs) {
|
|
228
|
-
return candidate;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
candidateDate = addDays(candidateDate, 1);
|
|
232
|
-
}
|
|
233
|
-
return void 0;
|
|
234
|
-
}
|
|
235
|
-
function getMonthlyNextRunAtMs(args) {
|
|
236
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
237
|
-
const dayOfMonth = args.recurrence.dayOfMonth;
|
|
238
|
-
if (!start || !dayOfMonth) {
|
|
239
|
-
return void 0;
|
|
240
|
-
}
|
|
241
|
-
const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
|
|
242
|
-
let monthIndex = scheduledDate.year * 12 + scheduledDate.month - 1;
|
|
243
|
-
const startMonthIndex = start.year * 12 + start.month - 1;
|
|
244
|
-
for (let attempts = 0; attempts < 1200; attempts += 1) {
|
|
245
|
-
monthIndex += args.recurrence.interval;
|
|
246
|
-
if (monthIndex < startMonthIndex) {
|
|
247
|
-
monthIndex = startMonthIndex;
|
|
248
|
-
}
|
|
249
|
-
const year = Math.floor(monthIndex / 12);
|
|
250
|
-
const month = monthIndex % 12 + 1;
|
|
251
|
-
if (dayOfMonth > daysInMonth(year, month)) {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
const candidate = buildCandidate({
|
|
255
|
-
date: { year, month, day: dayOfMonth },
|
|
256
|
-
recurrence: args.recurrence,
|
|
257
|
-
timezone: args.timezone
|
|
258
|
-
});
|
|
259
|
-
if (candidate > args.afterMs) {
|
|
260
|
-
return candidate;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return void 0;
|
|
264
|
-
}
|
|
265
|
-
function getYearlyNextRunAtMs(args) {
|
|
266
|
-
const start = parseLocalDate(args.recurrence.startDate);
|
|
267
|
-
const month = args.recurrence.month;
|
|
268
|
-
const dayOfMonth = args.recurrence.dayOfMonth;
|
|
269
|
-
if (!start || !month || !dayOfMonth) {
|
|
270
|
-
return void 0;
|
|
271
|
-
}
|
|
272
|
-
const scheduledDate = getLocalDate(args.scheduledForMs, args.timezone);
|
|
273
|
-
let year = scheduledDate.year;
|
|
274
|
-
for (let attempts = 0; attempts < 100; attempts += 1) {
|
|
275
|
-
year += args.recurrence.interval;
|
|
276
|
-
if (year < start.year) {
|
|
277
|
-
year = start.year;
|
|
278
|
-
}
|
|
279
|
-
if (dayOfMonth > daysInMonth(year, month)) {
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
const candidate = buildCandidate({
|
|
283
|
-
date: { year, month, day: dayOfMonth },
|
|
284
|
-
recurrence: args.recurrence,
|
|
285
|
-
timezone: args.timezone
|
|
286
|
-
});
|
|
287
|
-
if (candidate > args.afterMs) {
|
|
288
|
-
return candidate;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return void 0;
|
|
292
|
-
}
|
|
293
|
-
function buildCalendarRecurrence(args) {
|
|
294
|
-
const interval = args.interval && args.interval > 0 ? args.interval : 1;
|
|
295
|
-
const parts = getZonedDateTimeParts(args.nextRunAtMs, args.timezone);
|
|
296
|
-
const time = { hour: parts.hour, minute: parts.minute };
|
|
297
|
-
const startDate = formatLocalDate(parts);
|
|
298
|
-
if (args.frequency === "weekly") {
|
|
299
|
-
const weekdays = normalizeWeekdays(args.weekdays);
|
|
300
|
-
return {
|
|
301
|
-
frequency: args.frequency,
|
|
302
|
-
interval,
|
|
303
|
-
startDate,
|
|
304
|
-
time,
|
|
305
|
-
weekdays: weekdays.length > 0 ? weekdays : [parts.weekday]
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
if (args.frequency === "monthly") {
|
|
309
|
-
return {
|
|
310
|
-
dayOfMonth: parts.day,
|
|
311
|
-
frequency: args.frequency,
|
|
312
|
-
interval,
|
|
313
|
-
startDate,
|
|
314
|
-
time
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
if (args.frequency === "yearly") {
|
|
318
|
-
return {
|
|
319
|
-
dayOfMonth: parts.day,
|
|
320
|
-
frequency: args.frequency,
|
|
321
|
-
interval,
|
|
322
|
-
month: parts.month,
|
|
323
|
-
startDate,
|
|
324
|
-
time
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
frequency: args.frequency,
|
|
329
|
-
interval,
|
|
330
|
-
startDate,
|
|
331
|
-
time
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
296
|
function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
|
|
335
297
|
if (task.schedule.kind !== "recurring") {
|
|
336
298
|
return void 0;
|
|
@@ -339,35 +301,14 @@ function getNextRunAtMs(task, scheduledForMs, afterMs = scheduledForMs) {
|
|
|
339
301
|
if (!recurrence || !Number.isFinite(recurrence.interval) || recurrence.interval <= 0) {
|
|
340
302
|
return void 0;
|
|
341
303
|
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
afterMs
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
if (recurrence.frequency === "weekly") {
|
|
351
|
-
return getWeeklyNextRunAtMs({
|
|
352
|
-
recurrence,
|
|
353
|
-
timezone: task.schedule.timezone,
|
|
354
|
-
scheduledForMs,
|
|
355
|
-
afterMs
|
|
356
|
-
});
|
|
357
|
-
}
|
|
358
|
-
if (recurrence.frequency === "monthly") {
|
|
359
|
-
return getMonthlyNextRunAtMs({
|
|
360
|
-
recurrence,
|
|
361
|
-
timezone: task.schedule.timezone,
|
|
362
|
-
scheduledForMs,
|
|
363
|
-
afterMs
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
return getYearlyNextRunAtMs({
|
|
304
|
+
const timezone = task.schedule.timezone;
|
|
305
|
+
const afterDate = getLocalDate(afterMs, timezone);
|
|
306
|
+
const nextScheduledDate = addDays(getLocalDate(scheduledForMs, timezone), 1);
|
|
307
|
+
return findNextRunAtMs({
|
|
308
|
+
afterMs,
|
|
367
309
|
recurrence,
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
afterMs
|
|
310
|
+
searchFrom: compareDate(afterDate, nextScheduledDate) < 0 ? nextScheduledDate : afterDate,
|
|
311
|
+
timezone
|
|
371
312
|
});
|
|
372
313
|
}
|
|
373
314
|
|
|
@@ -538,11 +479,12 @@ function buildRunId(taskId, scheduledForMs) {
|
|
|
538
479
|
function unique(values) {
|
|
539
480
|
return [...new Set(values.filter(Boolean))];
|
|
540
481
|
}
|
|
482
|
+
var schedulerTaskIndexSchema = z2.array(z2.string().min(1));
|
|
483
|
+
function parseStringIndex(value) {
|
|
484
|
+
return value === void 0 ? [] : schedulerTaskIndexSchema.parse(value);
|
|
485
|
+
}
|
|
541
486
|
async function getIndex(state, key) {
|
|
542
|
-
|
|
543
|
-
return unique(
|
|
544
|
-
values.filter((value) => typeof value === "string")
|
|
545
|
-
);
|
|
487
|
+
return unique(parseStringIndex(await state.get(key)));
|
|
546
488
|
}
|
|
547
489
|
function isFinishedRun(run) {
|
|
548
490
|
return run.status === "completed" || run.status === "failed" || run.status === "blocked" || run.status === "skipped";
|
|
@@ -798,6 +740,17 @@ var SqlSchedulerStore = class {
|
|
|
798
740
|
this.db = db;
|
|
799
741
|
}
|
|
800
742
|
db;
|
|
743
|
+
async createTask(task) {
|
|
744
|
+
const next = requireStoredTask(task);
|
|
745
|
+
return await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
|
|
746
|
+
const current = await getTaskFromSql(db, task.id);
|
|
747
|
+
if (current) {
|
|
748
|
+
return current;
|
|
749
|
+
}
|
|
750
|
+
await this.saveTaskRecord(db, next, void 0);
|
|
751
|
+
return next;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
801
754
|
async saveTask(task) {
|
|
802
755
|
const next = requireStoredTask(task);
|
|
803
756
|
await withSqlLock(this.db, taskLockKey(task.id), async (db) => {
|
|
@@ -1181,55 +1134,304 @@ function scheduledTaskPrincipalLabel(principal) {
|
|
|
1181
1134
|
|
|
1182
1135
|
// src/tools/create-task.ts
|
|
1183
1136
|
import { definePluginTool } from "@sentry/junior-plugin-api";
|
|
1184
|
-
import { z as
|
|
1137
|
+
import { z as z5 } from "zod";
|
|
1138
|
+
|
|
1139
|
+
// src/schedule-intent.ts
|
|
1140
|
+
import { z as z3 } from "zod";
|
|
1141
|
+
var localDateSchema = z3.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use a local date in YYYY-MM-DD format.");
|
|
1142
|
+
var localTimeSchema = z3.string().regex(/^([01]\d|2[0-3]):[0-5]\d$/, "Use local time in HH:MM format.");
|
|
1143
|
+
var timezoneSchema = z3.string().min(1).max(80).describe(
|
|
1144
|
+
"IANA timezone, for example America/Los_Angeles. Omit or use null for the scheduler default."
|
|
1145
|
+
).nullable().optional();
|
|
1146
|
+
var weekdaySchema = z3.enum([
|
|
1147
|
+
"sunday",
|
|
1148
|
+
"monday",
|
|
1149
|
+
"tuesday",
|
|
1150
|
+
"wednesday",
|
|
1151
|
+
"thursday",
|
|
1152
|
+
"friday",
|
|
1153
|
+
"saturday"
|
|
1154
|
+
]);
|
|
1155
|
+
var oneOffScheduleIntentSchema = z3.object({
|
|
1156
|
+
kind: z3.literal("one_off").describe("A schedule that runs once."),
|
|
1157
|
+
timezone: timezoneSchema,
|
|
1158
|
+
timing: z3.discriminatedUnion("type", [
|
|
1159
|
+
z3.object({
|
|
1160
|
+
type: z3.literal("after").describe("Run after a relative delay from the server clock."),
|
|
1161
|
+
value: z3.number().int().positive().max(1e5),
|
|
1162
|
+
unit: z3.enum(["second", "minute", "hour", "day", "week"])
|
|
1163
|
+
}).strict(),
|
|
1164
|
+
z3.object({
|
|
1165
|
+
type: z3.literal("at").describe("Run at an explicit local calendar date and time."),
|
|
1166
|
+
date: localDateSchema.describe(
|
|
1167
|
+
"Requested local calendar date in YYYY-MM-DD format."
|
|
1168
|
+
),
|
|
1169
|
+
time: localTimeSchema.describe(
|
|
1170
|
+
"Requested local wall-clock time in HH:MM format."
|
|
1171
|
+
)
|
|
1172
|
+
}).strict()
|
|
1173
|
+
]).describe("Relative delay or explicit local time for the one-off run.")
|
|
1174
|
+
}).strict();
|
|
1175
|
+
var recurringScheduleIntentSchema = z3.object({
|
|
1176
|
+
kind: z3.literal("recurring").describe("A repeating calendar schedule."),
|
|
1177
|
+
frequency: z3.enum(["daily", "weekly", "monthly", "yearly"]).describe("Recurring tasks can run at most once per day."),
|
|
1178
|
+
interval: z3.number().int().positive().max(365).describe("Repeat every N frequency units. Defaults to 1.").nullable().optional(),
|
|
1179
|
+
time: localTimeSchema.describe(
|
|
1180
|
+
"Local wall-clock time for each occurrence in HH:MM format."
|
|
1181
|
+
),
|
|
1182
|
+
weekdays: z3.array(weekdaySchema).min(1).max(7).describe("Required for weekly schedules.").nullable().optional(),
|
|
1183
|
+
day_of_month: z3.number().int().min(1).max(31).describe("Required for monthly and yearly schedules.").nullable().optional(),
|
|
1184
|
+
month: z3.number().int().min(1).max(12).describe("Required for yearly schedules, where January is 1.").nullable().optional(),
|
|
1185
|
+
start_date: localDateSchema.describe(
|
|
1186
|
+
"Optional local calendar date that anchors the recurrence. Omit to start with the next matching occurrence."
|
|
1187
|
+
).nullable().optional(),
|
|
1188
|
+
timezone: timezoneSchema
|
|
1189
|
+
}).strict().superRefine((schedule, context) => {
|
|
1190
|
+
if (schedule.frequency === "weekly" && schedule.weekdays == null) {
|
|
1191
|
+
context.addIssue({
|
|
1192
|
+
code: "custom",
|
|
1193
|
+
message: "Weekly schedules require weekdays.",
|
|
1194
|
+
path: ["weekdays"]
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
if ((schedule.frequency === "monthly" || schedule.frequency === "yearly") && schedule.day_of_month == null) {
|
|
1198
|
+
context.addIssue({
|
|
1199
|
+
code: "custom",
|
|
1200
|
+
message: `${schedule.frequency} schedules require day_of_month.`,
|
|
1201
|
+
path: ["day_of_month"]
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
if (schedule.frequency === "yearly" && schedule.month == null) {
|
|
1205
|
+
context.addIssue({
|
|
1206
|
+
code: "custom",
|
|
1207
|
+
message: "Yearly schedules require month.",
|
|
1208
|
+
path: ["month"]
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
if (schedule.frequency !== "weekly" && schedule.weekdays != null) {
|
|
1212
|
+
context.addIssue({
|
|
1213
|
+
code: "custom",
|
|
1214
|
+
message: "weekdays applies only to weekly schedules.",
|
|
1215
|
+
path: ["weekdays"]
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
if (schedule.frequency !== "monthly" && schedule.frequency !== "yearly" && schedule.day_of_month != null) {
|
|
1219
|
+
context.addIssue({
|
|
1220
|
+
code: "custom",
|
|
1221
|
+
message: "day_of_month applies only to monthly or yearly schedules.",
|
|
1222
|
+
path: ["day_of_month"]
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
if (schedule.frequency !== "yearly" && schedule.month != null) {
|
|
1226
|
+
context.addIssue({
|
|
1227
|
+
code: "custom",
|
|
1228
|
+
message: "month applies only to yearly schedules.",
|
|
1229
|
+
path: ["month"]
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
var scheduleIntentSchema = z3.union([
|
|
1234
|
+
oneOffScheduleIntentSchema,
|
|
1235
|
+
recurringScheduleIntentSchema
|
|
1236
|
+
]);
|
|
1237
|
+
var WEEKDAY_INDEX = {
|
|
1238
|
+
sunday: 0,
|
|
1239
|
+
monday: 1,
|
|
1240
|
+
tuesday: 2,
|
|
1241
|
+
wednesday: 3,
|
|
1242
|
+
thursday: 4,
|
|
1243
|
+
friday: 5,
|
|
1244
|
+
saturday: 6
|
|
1245
|
+
};
|
|
1246
|
+
var DURATION_MS = {
|
|
1247
|
+
second: 1e3,
|
|
1248
|
+
minute: 60 * 1e3,
|
|
1249
|
+
hour: 60 * 60 * 1e3,
|
|
1250
|
+
day: 24 * 60 * 60 * 1e3,
|
|
1251
|
+
week: 7 * 24 * 60 * 60 * 1e3
|
|
1252
|
+
};
|
|
1253
|
+
function parseLocalTime(value) {
|
|
1254
|
+
const [hour, minute] = value.split(":").map(Number);
|
|
1255
|
+
return { hour, minute };
|
|
1256
|
+
}
|
|
1257
|
+
function localDateAt(timestampMs, timezone) {
|
|
1258
|
+
const parts = getZonedDateTimeParts(timestampMs, timezone);
|
|
1259
|
+
return [parts.year, parts.month, parts.day].map(
|
|
1260
|
+
(value, index2) => index2 === 0 ? String(value).padStart(4, "0") : String(value).padStart(2, "0")
|
|
1261
|
+
).join("-");
|
|
1262
|
+
}
|
|
1263
|
+
function isValidTimeZone(timezone) {
|
|
1264
|
+
try {
|
|
1265
|
+
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
1266
|
+
return true;
|
|
1267
|
+
} catch {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
function plural(value, unit) {
|
|
1272
|
+
return value === 1 ? unit : `${unit}s`;
|
|
1273
|
+
}
|
|
1274
|
+
function formatWeekdays(weekdays) {
|
|
1275
|
+
const labels = weekdays.map(
|
|
1276
|
+
(weekday) => `${weekday[0].toUpperCase()}${weekday.slice(1)}`
|
|
1277
|
+
);
|
|
1278
|
+
if (labels.length < 2) {
|
|
1279
|
+
return labels[0] ?? "";
|
|
1280
|
+
}
|
|
1281
|
+
return `${labels.slice(0, -1).join(", ")} and ${labels.at(-1)}`;
|
|
1282
|
+
}
|
|
1283
|
+
function recurringDescription(schedule, timezone) {
|
|
1284
|
+
const interval = schedule.interval ?? 1;
|
|
1285
|
+
const cadenceUnit = {
|
|
1286
|
+
daily: "day",
|
|
1287
|
+
weekly: "week",
|
|
1288
|
+
monthly: "month",
|
|
1289
|
+
yearly: "year"
|
|
1290
|
+
}[schedule.frequency];
|
|
1291
|
+
const cadence = interval === 1 ? cadenceUnit : `${interval} ${cadenceUnit}s`;
|
|
1292
|
+
let detail = "";
|
|
1293
|
+
if (schedule.frequency === "weekly") {
|
|
1294
|
+
detail = ` on ${formatWeekdays(schedule.weekdays)}`;
|
|
1295
|
+
} else if (schedule.frequency === "monthly") {
|
|
1296
|
+
detail = ` on day ${schedule.day_of_month}`;
|
|
1297
|
+
} else if (schedule.frequency === "yearly") {
|
|
1298
|
+
detail = ` on ${String(schedule.month).padStart(2, "0")}-${String(
|
|
1299
|
+
schedule.day_of_month
|
|
1300
|
+
).padStart(2, "0")}`;
|
|
1301
|
+
}
|
|
1302
|
+
return `Every ${cadence}${detail} at ${schedule.time} (${timezone})`;
|
|
1303
|
+
}
|
|
1304
|
+
var ScheduleIntentError = class extends Error {
|
|
1305
|
+
};
|
|
1306
|
+
function compileScheduleIntent(args) {
|
|
1307
|
+
const timezone = args.intent.timezone ?? args.defaultTimezone;
|
|
1308
|
+
if (!isValidTimeZone(timezone)) {
|
|
1309
|
+
throw new ScheduleIntentError("timezone must be a valid IANA time zone.");
|
|
1310
|
+
}
|
|
1311
|
+
if (args.intent.kind === "one_off") {
|
|
1312
|
+
if (args.intent.timing.type === "after") {
|
|
1313
|
+
const nextRunAtMs3 = args.nowMs + args.intent.timing.value * DURATION_MS[args.intent.timing.unit];
|
|
1314
|
+
return {
|
|
1315
|
+
nextRunAtMs: nextRunAtMs3,
|
|
1316
|
+
schedule: {
|
|
1317
|
+
description: `In ${args.intent.timing.value} ${plural(
|
|
1318
|
+
args.intent.timing.value,
|
|
1319
|
+
args.intent.timing.unit
|
|
1320
|
+
)}`,
|
|
1321
|
+
kind: "one_off",
|
|
1322
|
+
timezone
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
const nextRunAtMs2 = resolveLocalScheduleAtMs({
|
|
1327
|
+
date: args.intent.timing.date,
|
|
1328
|
+
time: parseLocalTime(args.intent.timing.time),
|
|
1329
|
+
timezone
|
|
1330
|
+
});
|
|
1331
|
+
if (nextRunAtMs2 === void 0) {
|
|
1332
|
+
throw new ScheduleIntentError(
|
|
1333
|
+
"The requested local date or time does not exist in that timezone."
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
if (nextRunAtMs2 <= args.nowMs) {
|
|
1337
|
+
throw new ScheduleIntentError(
|
|
1338
|
+
"The requested one-off schedule must be in the future."
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
return {
|
|
1342
|
+
nextRunAtMs: nextRunAtMs2,
|
|
1343
|
+
schedule: {
|
|
1344
|
+
description: `Once on ${args.intent.timing.date} at ${args.intent.timing.time} (${timezone})`,
|
|
1345
|
+
kind: "one_off",
|
|
1346
|
+
timezone
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
}
|
|
1350
|
+
const recurrence = {
|
|
1351
|
+
frequency: args.intent.frequency,
|
|
1352
|
+
interval: args.intent.interval ?? 1,
|
|
1353
|
+
startDate: args.intent.start_date ?? localDateAt(args.nowMs, timezone),
|
|
1354
|
+
time: parseLocalTime(args.intent.time),
|
|
1355
|
+
...args.intent.frequency === "weekly" ? {
|
|
1356
|
+
weekdays: [
|
|
1357
|
+
...new Set(
|
|
1358
|
+
args.intent.weekdays.map((weekday) => WEEKDAY_INDEX[weekday])
|
|
1359
|
+
)
|
|
1360
|
+
].sort((left, right) => left - right)
|
|
1361
|
+
} : {},
|
|
1362
|
+
...args.intent.frequency === "monthly" || args.intent.frequency === "yearly" ? { dayOfMonth: args.intent.day_of_month ?? void 0 } : {},
|
|
1363
|
+
...args.intent.frequency === "yearly" ? { month: args.intent.month ?? void 0 } : {}
|
|
1364
|
+
};
|
|
1365
|
+
const searchRecurrence = args.intent.start_date ? recurrence : { ...recurrence, interval: 1 };
|
|
1366
|
+
const nextRunAtMs = getFirstRunAtMs({
|
|
1367
|
+
afterMs: args.nowMs,
|
|
1368
|
+
recurrence: searchRecurrence,
|
|
1369
|
+
timezone
|
|
1370
|
+
});
|
|
1371
|
+
if (nextRunAtMs === void 0) {
|
|
1372
|
+
throw new ScheduleIntentError(
|
|
1373
|
+
"The recurring schedule has no valid future occurrence."
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const materializedRecurrence = args.intent.start_date ? recurrence : { ...recurrence, startDate: localDateAt(nextRunAtMs, timezone) };
|
|
1377
|
+
return {
|
|
1378
|
+
nextRunAtMs,
|
|
1379
|
+
schedule: {
|
|
1380
|
+
description: recurringDescription(args.intent, timezone),
|
|
1381
|
+
kind: "recurring",
|
|
1382
|
+
recurrence: materializedRecurrence,
|
|
1383
|
+
timezone
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1185
1387
|
|
|
1186
1388
|
// src/tool-support.ts
|
|
1187
|
-
import {
|
|
1389
|
+
import { createHash } from "crypto";
|
|
1188
1390
|
import {
|
|
1189
1391
|
PluginToolInputError,
|
|
1190
1392
|
pluginToolResultSchema,
|
|
1191
1393
|
sourceSchema
|
|
1192
1394
|
} from "@sentry/junior-plugin-api";
|
|
1193
|
-
import { z as
|
|
1395
|
+
import { z as z4 } from "zod";
|
|
1194
1396
|
var TASK_ID_PREFIX = "sched";
|
|
1195
1397
|
var MAX_LISTED_TASKS = 50;
|
|
1196
1398
|
var DEFAULT_SCHEDULE_TIMEZONE = "America/Los_Angeles";
|
|
1197
|
-
var compactTaskResultSchema =
|
|
1198
|
-
id:
|
|
1199
|
-
status:
|
|
1200
|
-
task:
|
|
1201
|
-
schedule:
|
|
1202
|
-
timezone:
|
|
1203
|
-
recurrence:
|
|
1204
|
-
next_run_at:
|
|
1205
|
-
conversation_access:
|
|
1206
|
-
credential_mode:
|
|
1207
|
-
last_run_at:
|
|
1208
|
-
run_now_at:
|
|
1399
|
+
var compactTaskResultSchema = z4.object({
|
|
1400
|
+
id: z4.string(),
|
|
1401
|
+
status: z4.enum(["active", "paused", "blocked", "deleted"]),
|
|
1402
|
+
task: z4.string(),
|
|
1403
|
+
schedule: z4.string(),
|
|
1404
|
+
timezone: z4.string(),
|
|
1405
|
+
recurrence: z4.unknown().nullable(),
|
|
1406
|
+
next_run_at: z4.string().nullable(),
|
|
1407
|
+
conversation_access: z4.unknown().nullable(),
|
|
1408
|
+
credential_mode: z4.enum(["system", "creator"]),
|
|
1409
|
+
last_run_at: z4.string().nullable(),
|
|
1410
|
+
run_now_at: z4.string().nullable()
|
|
1209
1411
|
}).strict();
|
|
1210
|
-
var scheduleTaskResultDataSchema =
|
|
1211
|
-
ok:
|
|
1412
|
+
var scheduleTaskResultDataSchema = z4.object({
|
|
1413
|
+
ok: z4.literal(true),
|
|
1212
1414
|
task: compactTaskResultSchema
|
|
1213
1415
|
}).strict();
|
|
1214
1416
|
var scheduleTaskToolResultSchema = pluginToolResultSchema.extend({
|
|
1215
|
-
ok:
|
|
1216
|
-
status:
|
|
1217
|
-
target:
|
|
1417
|
+
ok: z4.literal(true),
|
|
1418
|
+
status: z4.literal("success"),
|
|
1419
|
+
target: z4.string(),
|
|
1218
1420
|
data: scheduleTaskResultDataSchema,
|
|
1219
1421
|
task: compactTaskResultSchema
|
|
1220
1422
|
});
|
|
1221
|
-
var scheduleListResultDataSchema =
|
|
1222
|
-
ok:
|
|
1223
|
-
tasks:
|
|
1224
|
-
truncated:
|
|
1423
|
+
var scheduleListResultDataSchema = z4.object({
|
|
1424
|
+
ok: z4.literal(true),
|
|
1425
|
+
tasks: z4.array(compactTaskResultSchema),
|
|
1426
|
+
truncated: z4.boolean()
|
|
1225
1427
|
}).strict();
|
|
1226
1428
|
var scheduleListToolResultSchema = pluginToolResultSchema.extend({
|
|
1227
|
-
ok:
|
|
1228
|
-
status:
|
|
1229
|
-
target:
|
|
1429
|
+
ok: z4.literal(true),
|
|
1430
|
+
status: z4.literal("success"),
|
|
1431
|
+
target: z4.string(),
|
|
1230
1432
|
data: scheduleListResultDataSchema,
|
|
1231
|
-
tasks:
|
|
1232
|
-
truncated:
|
|
1433
|
+
tasks: z4.array(compactTaskResultSchema),
|
|
1434
|
+
truncated: z4.boolean()
|
|
1233
1435
|
});
|
|
1234
1436
|
function throwToolInputError(error) {
|
|
1235
1437
|
throw new PluginToolInputError(error);
|
|
@@ -1363,8 +1565,21 @@ function scheduleListToolResult(args) {
|
|
|
1363
1565
|
truncated: args.truncated
|
|
1364
1566
|
};
|
|
1365
1567
|
}
|
|
1366
|
-
function buildTaskId() {
|
|
1367
|
-
|
|
1568
|
+
function buildTaskId(args) {
|
|
1569
|
+
const toolCallId = args.toolCallId?.trim();
|
|
1570
|
+
if (!toolCallId) {
|
|
1571
|
+
throw new Error("Scheduler task creation requires a tool-call identity.");
|
|
1572
|
+
}
|
|
1573
|
+
const digest = createHash("sha256").update(
|
|
1574
|
+
JSON.stringify({
|
|
1575
|
+
actor: args.actor.slackUserId,
|
|
1576
|
+
channel: args.destination.channelId,
|
|
1577
|
+
operation: toolCallId,
|
|
1578
|
+
platform: args.destination.platform,
|
|
1579
|
+
team: args.destination.teamId
|
|
1580
|
+
})
|
|
1581
|
+
).digest("hex").slice(0, 32);
|
|
1582
|
+
return `${TASK_ID_PREFIX}_${digest}`;
|
|
1368
1583
|
}
|
|
1369
1584
|
function schedulerStore(context) {
|
|
1370
1585
|
return context.store;
|
|
@@ -1375,133 +1590,64 @@ function normalizeStatus(value) {
|
|
|
1375
1590
|
}
|
|
1376
1591
|
return void 0;
|
|
1377
1592
|
}
|
|
1378
|
-
function normalizeFrequency(value) {
|
|
1379
|
-
if (value === "daily" || value === "weekly" || value === "monthly" || value === "yearly") {
|
|
1380
|
-
return value;
|
|
1381
|
-
}
|
|
1382
|
-
return void 0;
|
|
1383
|
-
}
|
|
1384
|
-
function buildRecurrence(args) {
|
|
1385
|
-
if (args.input.recurrence === null) {
|
|
1386
|
-
return void 0;
|
|
1387
|
-
}
|
|
1388
|
-
const frequency = normalizeFrequency(args.input.recurrence) ?? args.existing?.frequency;
|
|
1389
|
-
if (!frequency) {
|
|
1390
|
-
return void 0;
|
|
1391
|
-
}
|
|
1392
|
-
if (!args.nextRunAtMs) {
|
|
1393
|
-
throwToolInputError("Recurring scheduled tasks require next_run_at.");
|
|
1394
|
-
}
|
|
1395
|
-
try {
|
|
1396
|
-
return buildCalendarRecurrence({
|
|
1397
|
-
frequency,
|
|
1398
|
-
interval: args.existing?.interval,
|
|
1399
|
-
nextRunAtMs: args.nextRunAtMs,
|
|
1400
|
-
timezone: args.timezone,
|
|
1401
|
-
weekdays: frequency === "weekly" ? args.existing?.weekdays : void 0
|
|
1402
|
-
});
|
|
1403
|
-
} catch (error) {
|
|
1404
|
-
throwToolInputError(
|
|
1405
|
-
error instanceof RangeError ? "timezone must be a valid IANA time zone." : error instanceof Error ? error.message : String(error)
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
function validateRecurringFrequencyLimit(input) {
|
|
1410
|
-
if (input.recurrence !== void 0 && input.recurrence !== null && !normalizeFrequency(input.recurrence)) {
|
|
1411
|
-
throwToolInputError(
|
|
1412
|
-
"Recurring scheduled tasks can run at most once per day."
|
|
1413
|
-
);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
function validateCreateScheduleKind(input) {
|
|
1417
|
-
if (input.schedule_kind === void 0) {
|
|
1418
|
-
throwToolInputError("Provide schedule_kind as one_off or recurring.");
|
|
1419
|
-
}
|
|
1420
|
-
if (input.schedule_kind !== "one_off" && input.schedule_kind !== "recurring") {
|
|
1421
|
-
throwToolInputError("schedule_kind must be one_off or recurring.");
|
|
1422
|
-
}
|
|
1423
|
-
if (input.schedule_kind === "one_off" && input.recurrence !== void 0 && input.recurrence !== null) {
|
|
1424
|
-
throwToolInputError("Omit recurrence when schedule_kind is one_off.");
|
|
1425
|
-
}
|
|
1426
|
-
if (input.schedule_kind === "recurring" && (input.recurrence === void 0 || input.recurrence === null)) {
|
|
1427
|
-
throwToolInputError("Provide recurrence when schedule_kind is recurring.");
|
|
1428
|
-
}
|
|
1429
|
-
}
|
|
1430
|
-
function shouldRebuildRecurrence(input) {
|
|
1431
|
-
return input.next_run_at !== void 0 || input.recurrence !== void 0 || input.timezone !== void 0;
|
|
1432
|
-
}
|
|
1433
1593
|
function getDefaultScheduleTimezone() {
|
|
1434
1594
|
return process.env.JUNIOR_TIMEZONE?.trim() || DEFAULT_SCHEDULE_TIMEZONE;
|
|
1435
1595
|
}
|
|
1436
|
-
function isValidTimeZone(timezone) {
|
|
1437
|
-
try {
|
|
1438
|
-
new Intl.DateTimeFormat("en-US", { timeZone: timezone }).format();
|
|
1439
|
-
return true;
|
|
1440
|
-
} catch {
|
|
1441
|
-
return false;
|
|
1442
|
-
}
|
|
1443
|
-
}
|
|
1444
|
-
function parseNextRunAtMs(nextRunAtIso) {
|
|
1445
|
-
try {
|
|
1446
|
-
if (nextRunAtIso) {
|
|
1447
|
-
return parseScheduleTimestamp(nextRunAtIso);
|
|
1448
|
-
}
|
|
1449
|
-
} catch {
|
|
1450
|
-
return void 0;
|
|
1451
|
-
}
|
|
1452
|
-
return void 0;
|
|
1453
|
-
}
|
|
1454
1596
|
|
|
1455
1597
|
// src/tools/create-task.ts
|
|
1456
1598
|
function createSlackScheduleCreateTaskTool(context) {
|
|
1457
1599
|
return definePluginTool({
|
|
1458
|
-
description: "Create a one-time or recurring Junior task in the active Slack conversation
|
|
1600
|
+
description: "Create a one-time or recurring Junior task in the active Slack conversation when the user asks Junior to do work later or repeatedly.",
|
|
1459
1601
|
executionMode: "sequential",
|
|
1460
|
-
inputSchema:
|
|
1461
|
-
task:
|
|
1462
|
-
schedule:
|
|
1463
|
-
|
|
1464
|
-
"Required schedule classification. Use one_off for one-time reminders or one-time scheduled work. Use recurring only when the user explicitly asks for a repeating schedule."
|
|
1602
|
+
inputSchema: z5.object({
|
|
1603
|
+
task: z5.string().min(1).max(4e3),
|
|
1604
|
+
schedule: scheduleIntentSchema.describe(
|
|
1605
|
+
"When the task runs. The scheduler computes the exact next run from this intent and the server clock."
|
|
1465
1606
|
),
|
|
1466
|
-
|
|
1467
|
-
"IANA timezone, e.g. 'America/Los_Angeles'. Defaults to the channel's configured timezone."
|
|
1468
|
-
).optional(),
|
|
1469
|
-
next_run_at: z4.string().min(1).describe(
|
|
1470
|
-
"Exact next run time as an ISO timestamp, computed from the user's requested schedule."
|
|
1471
|
-
).optional(),
|
|
1472
|
-
recurrence: z4.enum(["daily", "weekly", "monthly", "yearly"]).nullable().describe(
|
|
1473
|
-
"Required when schedule_kind is recurring. Omit when schedule_kind is one_off. Recurring tasks run at most once per day: use daily, weekly, monthly, or yearly only."
|
|
1474
|
-
).optional(),
|
|
1475
|
-
credential_mode: z4.enum(["system", "creator"]).nullable().describe(
|
|
1607
|
+
credential_mode: z5.enum(["system", "creator"]).nullable().describe(
|
|
1476
1608
|
"Use creator only when the current user explicitly authorizes future scheduled use of their connected credentials. Omit or use system otherwise."
|
|
1477
1609
|
).optional()
|
|
1478
|
-
}),
|
|
1610
|
+
}).strict(),
|
|
1479
1611
|
outputSchema: scheduleTaskToolResultSchema,
|
|
1480
|
-
execute: async (input) => {
|
|
1612
|
+
execute: async (input, options) => {
|
|
1481
1613
|
const destination = requireActiveConversation(context);
|
|
1482
1614
|
const actor = requireActor(context, destination);
|
|
1483
|
-
const
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1615
|
+
const store = schedulerStore(context);
|
|
1616
|
+
const id = buildTaskId({
|
|
1617
|
+
actor,
|
|
1618
|
+
destination,
|
|
1619
|
+
toolCallId: options.toolCallId
|
|
1620
|
+
});
|
|
1621
|
+
const existing = await store.getTask(id);
|
|
1622
|
+
if (existing) {
|
|
1623
|
+
if (!sameDestination(existing, destination) || existing.createdBy.slackUserId !== actor.slackUserId) {
|
|
1624
|
+
throwToolInputError("Scheduled task operation identity is invalid.");
|
|
1625
|
+
}
|
|
1626
|
+
return scheduleTaskToolResult(
|
|
1627
|
+
"slackScheduleCreateTask",
|
|
1628
|
+
compactTask(existing)
|
|
1629
|
+
);
|
|
1489
1630
|
}
|
|
1490
|
-
const
|
|
1491
|
-
|
|
1492
|
-
|
|
1631
|
+
const nowMs = context.now?.() ?? Date.now();
|
|
1632
|
+
let compiled;
|
|
1633
|
+
try {
|
|
1634
|
+
compiled = compileScheduleIntent({
|
|
1635
|
+
defaultTimezone: getDefaultScheduleTimezone(),
|
|
1636
|
+
intent: input.schedule,
|
|
1637
|
+
nowMs
|
|
1638
|
+
});
|
|
1639
|
+
} catch (error) {
|
|
1640
|
+
if (error instanceof ScheduleIntentError) {
|
|
1641
|
+
throwToolInputError(error.message);
|
|
1642
|
+
}
|
|
1643
|
+
throw error;
|
|
1493
1644
|
}
|
|
1494
|
-
const recurrence = buildRecurrence({
|
|
1495
|
-
input,
|
|
1496
|
-
nextRunAtMs,
|
|
1497
|
-
timezone
|
|
1498
|
-
});
|
|
1499
1645
|
const conversationAccess = getConversationAccess(
|
|
1500
1646
|
destination,
|
|
1501
1647
|
context.source
|
|
1502
1648
|
);
|
|
1503
1649
|
const task = {
|
|
1504
|
-
id
|
|
1650
|
+
id,
|
|
1505
1651
|
createdAtMs: nowMs,
|
|
1506
1652
|
updatedAtMs: nowMs,
|
|
1507
1653
|
createdBy: actor,
|
|
@@ -1509,23 +1655,21 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1509
1655
|
credentialMode: input.credential_mode ?? "system",
|
|
1510
1656
|
destination,
|
|
1511
1657
|
executionActor: SCHEDULED_TASK_SYSTEM_ACTOR,
|
|
1512
|
-
nextRunAtMs,
|
|
1658
|
+
nextRunAtMs: compiled.nextRunAtMs,
|
|
1513
1659
|
originalRequest: context.userText,
|
|
1514
|
-
schedule:
|
|
1515
|
-
description: input.schedule,
|
|
1516
|
-
timezone,
|
|
1517
|
-
kind: recurrence ? "recurring" : "one_off",
|
|
1518
|
-
recurrence
|
|
1519
|
-
},
|
|
1660
|
+
schedule: compiled.schedule,
|
|
1520
1661
|
status: "active",
|
|
1521
1662
|
task: {
|
|
1522
1663
|
text: input.task
|
|
1523
1664
|
}
|
|
1524
1665
|
};
|
|
1525
|
-
await
|
|
1666
|
+
const committed = await store.createTask(task);
|
|
1667
|
+
if (!sameDestination(committed, destination) || committed.createdBy.slackUserId !== actor.slackUserId) {
|
|
1668
|
+
throwToolInputError("Scheduled task operation identity is invalid.");
|
|
1669
|
+
}
|
|
1526
1670
|
return scheduleTaskToolResult(
|
|
1527
1671
|
"slackScheduleCreateTask",
|
|
1528
|
-
compactTask(
|
|
1672
|
+
compactTask(committed)
|
|
1529
1673
|
);
|
|
1530
1674
|
}
|
|
1531
1675
|
});
|
|
@@ -1533,13 +1677,13 @@ function createSlackScheduleCreateTaskTool(context) {
|
|
|
1533
1677
|
|
|
1534
1678
|
// src/tools/delete-task.ts
|
|
1535
1679
|
import { definePluginTool as definePluginTool2 } from "@sentry/junior-plugin-api";
|
|
1536
|
-
import { z as
|
|
1680
|
+
import { z as z6 } from "zod";
|
|
1537
1681
|
function createSlackScheduleDeleteTaskTool(context) {
|
|
1538
1682
|
return definePluginTool2({
|
|
1539
1683
|
description: "Delete one scheduled Junior task from the active Slack conversation. Use only task IDs returned for this conversation. Do not delete schedules from threads, other channels, or another user's DM.",
|
|
1540
1684
|
executionMode: "sequential",
|
|
1541
|
-
inputSchema:
|
|
1542
|
-
task_id:
|
|
1685
|
+
inputSchema: z6.object({
|
|
1686
|
+
task_id: z6.string().min(1).describe(
|
|
1543
1687
|
"ID of the task to delete. Must be from this active Slack conversation."
|
|
1544
1688
|
)
|
|
1545
1689
|
}),
|
|
@@ -1564,12 +1708,12 @@ function createSlackScheduleDeleteTaskTool(context) {
|
|
|
1564
1708
|
|
|
1565
1709
|
// src/tools/list-tasks.ts
|
|
1566
1710
|
import { definePluginTool as definePluginTool3 } from "@sentry/junior-plugin-api";
|
|
1567
|
-
import { z as
|
|
1711
|
+
import { z as z7 } from "zod";
|
|
1568
1712
|
function createSlackScheduleListTasksTool(context) {
|
|
1569
1713
|
return definePluginTool3({
|
|
1570
1714
|
description: "List scheduled Junior tasks for the active Slack conversation. Use when the user asks what is scheduled here, or when task IDs are needed before editing, deleting, or running schedules. Only manages tasks for the active Slack DM or channel.",
|
|
1571
1715
|
annotations: { readOnlyHint: true, destructiveHint: false },
|
|
1572
|
-
inputSchema:
|
|
1716
|
+
inputSchema: z7.object({}),
|
|
1573
1717
|
outputSchema: scheduleListToolResultSchema,
|
|
1574
1718
|
execute: async () => {
|
|
1575
1719
|
const destination = requireActiveConversation(context);
|
|
@@ -1591,13 +1735,13 @@ function createSlackScheduleListTasksTool(context) {
|
|
|
1591
1735
|
|
|
1592
1736
|
// src/tools/run-task-now.ts
|
|
1593
1737
|
import { definePluginTool as definePluginTool4 } from "@sentry/junior-plugin-api";
|
|
1594
|
-
import { z as
|
|
1738
|
+
import { z as z8 } from "zod";
|
|
1595
1739
|
function createSlackScheduleRunTaskNowTool(context) {
|
|
1596
1740
|
return definePluginTool4({
|
|
1597
1741
|
description: "Queue an existing active scheduled Junior task to run as soon as possible, without changing its cadence. Use when the user asks to run an existing scheduled task now. Use only task IDs returned for this conversation.",
|
|
1598
1742
|
executionMode: "sequential",
|
|
1599
|
-
inputSchema:
|
|
1600
|
-
task_id:
|
|
1743
|
+
inputSchema: z8.object({
|
|
1744
|
+
task_id: z8.string().min(1).describe(
|
|
1601
1745
|
"ID of the active task to run now. Must be from this active Slack conversation."
|
|
1602
1746
|
)
|
|
1603
1747
|
}),
|
|
@@ -1626,29 +1770,26 @@ function createSlackScheduleRunTaskNowTool(context) {
|
|
|
1626
1770
|
|
|
1627
1771
|
// src/tools/update-task.ts
|
|
1628
1772
|
import { definePluginTool as definePluginTool5 } from "@sentry/junior-plugin-api";
|
|
1629
|
-
import { z as
|
|
1773
|
+
import { z as z9 } from "zod";
|
|
1630
1774
|
function createSlackScheduleUpdateTaskTool(context) {
|
|
1631
1775
|
return definePluginTool5({
|
|
1632
|
-
description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation.
|
|
1776
|
+
description: "Edit, pause, resume, reschedule, or change credential use for an existing Junior scheduled task in the active Slack conversation.",
|
|
1633
1777
|
executionMode: "sequential",
|
|
1634
|
-
inputSchema:
|
|
1635
|
-
task_id:
|
|
1778
|
+
inputSchema: z9.object({
|
|
1779
|
+
task_id: z9.string().min(1).describe(
|
|
1636
1780
|
"ID of the task to update. Must be from this active Slack conversation."
|
|
1637
1781
|
),
|
|
1638
|
-
task:
|
|
1639
|
-
schedule:
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
"Provide only for repeating schedules. Omit for one-time requests. Set to null to convert a recurring task to one-time."
|
|
1644
|
-
).optional(),
|
|
1645
|
-
status: z8.enum(["active", "paused", "blocked"]).describe(
|
|
1782
|
+
task: z9.string().min(1).max(4e3).optional(),
|
|
1783
|
+
schedule: scheduleIntentSchema.describe(
|
|
1784
|
+
"Complete replacement schedule when rescheduling. Omit for task, status, or credential-only changes; the scheduler computes the next run."
|
|
1785
|
+
).nullable().optional(),
|
|
1786
|
+
status: z9.enum(["active", "paused", "blocked"]).describe(
|
|
1646
1787
|
"Set to active, paused, or blocked to resume, pause, or block the task."
|
|
1647
1788
|
).optional(),
|
|
1648
|
-
credential_mode:
|
|
1789
|
+
credential_mode: z9.enum(["system", "creator"]).nullable().describe(
|
|
1649
1790
|
"Set creator only when the current actor is the task creator and explicitly authorizes future scheduled credential use. Set system to disable delegation."
|
|
1650
1791
|
).optional()
|
|
1651
|
-
}),
|
|
1792
|
+
}).strict(),
|
|
1652
1793
|
outputSchema: scheduleTaskToolResultSchema,
|
|
1653
1794
|
execute: async (input) => {
|
|
1654
1795
|
const lookup = await getWritableTask({
|
|
@@ -1662,48 +1803,43 @@ function createSlackScheduleUpdateTaskTool(context) {
|
|
|
1662
1803
|
"Only the scheduled task creator can enable creator credential use."
|
|
1663
1804
|
);
|
|
1664
1805
|
}
|
|
1665
|
-
const
|
|
1666
|
-
|
|
1667
|
-
if (
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1806
|
+
const nowMs = context.now?.() ?? Date.now();
|
|
1807
|
+
let compiled;
|
|
1808
|
+
if (input.schedule) {
|
|
1809
|
+
try {
|
|
1810
|
+
compiled = compileScheduleIntent({
|
|
1811
|
+
defaultTimezone: lookup.schedule.timezone || getDefaultScheduleTimezone(),
|
|
1812
|
+
intent: input.schedule,
|
|
1813
|
+
nowMs
|
|
1814
|
+
});
|
|
1815
|
+
} catch (error) {
|
|
1816
|
+
if (error instanceof ScheduleIntentError) {
|
|
1817
|
+
throwToolInputError(error.message);
|
|
1818
|
+
}
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1674
1821
|
}
|
|
1822
|
+
const nextRunAtMs = compiled?.nextRunAtMs ?? lookup.nextRunAtMs;
|
|
1675
1823
|
const status = normalizeStatus(input.status);
|
|
1676
1824
|
if (input.status && !status) {
|
|
1677
1825
|
throwToolInputError("status must be active, paused, or blocked.");
|
|
1678
1826
|
}
|
|
1679
1827
|
if (status === "active" && !nextRunAtMs) {
|
|
1680
1828
|
throwToolInputError(
|
|
1681
|
-
"Active scheduled tasks require
|
|
1829
|
+
"Active scheduled tasks require a schedule with a future occurrence."
|
|
1682
1830
|
);
|
|
1683
1831
|
}
|
|
1684
|
-
const recurrence = shouldRebuildRecurrence(input) ? buildRecurrence({
|
|
1685
|
-
existing: lookup.schedule.recurrence,
|
|
1686
|
-
input,
|
|
1687
|
-
nextRunAtMs,
|
|
1688
|
-
timezone
|
|
1689
|
-
}) : lookup.schedule.recurrence;
|
|
1690
1832
|
const nextStatus = status ?? lookup.status;
|
|
1691
1833
|
const credentialMode = input.task !== void 0 && input.task !== lookup.task.text && !isCreator ? "system" : input.credential_mode ?? lookup.credentialMode;
|
|
1692
1834
|
const next = {
|
|
1693
1835
|
...lookup,
|
|
1694
1836
|
credentialMode,
|
|
1695
|
-
updatedAtMs:
|
|
1837
|
+
updatedAtMs: nowMs,
|
|
1696
1838
|
nextRunAtMs,
|
|
1697
|
-
runNowAtMs: nextStatus === "active" ? lookup.runNowAtMs : void 0,
|
|
1839
|
+
runNowAtMs: nextStatus === "active" && !compiled ? lookup.runNowAtMs : void 0,
|
|
1698
1840
|
status: nextStatus,
|
|
1699
1841
|
statusReason: nextStatus === "blocked" ? lookup.statusReason : void 0,
|
|
1700
|
-
schedule:
|
|
1701
|
-
...lookup.schedule,
|
|
1702
|
-
description: input.schedule ?? lookup.schedule.description,
|
|
1703
|
-
timezone,
|
|
1704
|
-
kind: recurrence ? "recurring" : "one_off",
|
|
1705
|
-
recurrence
|
|
1706
|
-
},
|
|
1842
|
+
schedule: compiled?.schedule ?? lookup.schedule,
|
|
1707
1843
|
task: input.task ? { text: input.task } : lookup.task
|
|
1708
1844
|
};
|
|
1709
1845
|
await schedulerStore(context).saveTask(next);
|