zardbot-telegram 1.0.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.
Files changed (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,239 @@
1
+ const BADGE_DATE_LOCALE = "en-US";
2
+ const WEEKDAY_LABELS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3
+ const zonedDateTimeFormatterCache = new Map();
4
+ const zonedYearFormatterCache = new Map();
5
+ function getZonedDateTimeFormatter(timezone) {
6
+ const cached = zonedDateTimeFormatterCache.get(timezone);
7
+ if (cached) {
8
+ return cached;
9
+ }
10
+ const formatter = new Intl.DateTimeFormat(BADGE_DATE_LOCALE, {
11
+ timeZone: timezone,
12
+ year: "numeric",
13
+ month: "short",
14
+ day: "numeric",
15
+ hour: "2-digit",
16
+ minute: "2-digit",
17
+ hour12: false,
18
+ hourCycle: "h23",
19
+ });
20
+ zonedDateTimeFormatterCache.set(timezone, formatter);
21
+ return formatter;
22
+ }
23
+ function getZonedYearFormatter(timezone) {
24
+ const cached = zonedYearFormatterCache.get(timezone);
25
+ if (cached) {
26
+ return cached;
27
+ }
28
+ const formatter = new Intl.DateTimeFormat(BADGE_DATE_LOCALE, {
29
+ timeZone: timezone,
30
+ year: "numeric",
31
+ });
32
+ zonedYearFormatterCache.set(timezone, formatter);
33
+ return formatter;
34
+ }
35
+ function getDateTimePart(parts, type) {
36
+ return parts.find((part) => part.type === type)?.value ?? "";
37
+ }
38
+ function getZonedDateParts(date, timezone) {
39
+ const parts = getZonedDateTimeFormatter(timezone).formatToParts(date);
40
+ const year = Number.parseInt(getDateTimePart(parts, "year"), 10);
41
+ const month = getDateTimePart(parts, "month");
42
+ const day = Number.parseInt(getDateTimePart(parts, "day"), 10);
43
+ const hour = Number.parseInt(getDateTimePart(parts, "hour"), 10);
44
+ const minute = Number.parseInt(getDateTimePart(parts, "minute"), 10);
45
+ if (!Number.isInteger(year) ||
46
+ !month ||
47
+ !Number.isInteger(day) ||
48
+ !Number.isInteger(hour) ||
49
+ !Number.isInteger(minute)) {
50
+ return null;
51
+ }
52
+ return {
53
+ year,
54
+ month,
55
+ day,
56
+ hour,
57
+ minute,
58
+ };
59
+ }
60
+ function padNumber(value) {
61
+ return value.toString().padStart(2, "0");
62
+ }
63
+ function formatTime(hour, minute) {
64
+ return `${padNumber(hour)}:${padNumber(minute)}`;
65
+ }
66
+ function formatOnceTaskBadge(runAt, timezone) {
67
+ const runAtDate = new Date(runAt);
68
+ if (Number.isNaN(runAtDate.getTime())) {
69
+ return runAt;
70
+ }
71
+ const runAtParts = getZonedDateParts(runAtDate, timezone);
72
+ if (!runAtParts) {
73
+ return runAt;
74
+ }
75
+ const currentYear = getZonedYearFormatter(timezone).format(new Date());
76
+ const includeYear = currentYear !== String(runAtParts.year);
77
+ const dateLabel = includeYear
78
+ ? `${runAtParts.day} ${runAtParts.month} ${runAtParts.year}`
79
+ : `${runAtParts.day} ${runAtParts.month}`;
80
+ return `${dateLabel} ${formatTime(runAtParts.hour, runAtParts.minute)}`;
81
+ }
82
+ function parseCronParts(cron) {
83
+ const parts = cron.trim().split(/\s+/);
84
+ if (parts.length !== 5) {
85
+ return null;
86
+ }
87
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
88
+ return {
89
+ minute,
90
+ hour,
91
+ dayOfMonth,
92
+ month,
93
+ dayOfWeek,
94
+ };
95
+ }
96
+ function parseExactNumber(field, min, max) {
97
+ const trimmedField = field.trim();
98
+ if (!/^\d+$/.test(trimmedField)) {
99
+ return null;
100
+ }
101
+ const value = Number.parseInt(trimmedField, 10);
102
+ if (!Number.isInteger(value) || value < min || value > max) {
103
+ return null;
104
+ }
105
+ return value;
106
+ }
107
+ function parseEveryStep(field, minStep, maxStep) {
108
+ const match = field.trim().match(/^\*\/(\d+)$/);
109
+ if (!match) {
110
+ return null;
111
+ }
112
+ const step = Number.parseInt(match[1], 10);
113
+ if (!Number.isInteger(step) || step < minStep || step > maxStep) {
114
+ return null;
115
+ }
116
+ return step;
117
+ }
118
+ function normalizeWeekdayValue(token) {
119
+ const normalized = token.trim().toLowerCase();
120
+ const aliases = {
121
+ sun: 0,
122
+ mon: 1,
123
+ tue: 2,
124
+ wed: 3,
125
+ thu: 4,
126
+ fri: 5,
127
+ sat: 6,
128
+ };
129
+ if (normalized in aliases) {
130
+ return aliases[normalized];
131
+ }
132
+ const numericValue = parseExactNumber(normalized, 0, 7);
133
+ if (numericValue === null) {
134
+ return null;
135
+ }
136
+ return numericValue === 7 ? 0 : numericValue;
137
+ }
138
+ function parseWeekdaySet(field) {
139
+ const values = new Set();
140
+ for (const token of field.split(",")) {
141
+ const trimmedToken = token.trim();
142
+ if (!trimmedToken || trimmedToken.includes("/")) {
143
+ return null;
144
+ }
145
+ if (trimmedToken.includes("-")) {
146
+ const [startRaw, endRaw] = trimmedToken.split("-");
147
+ const start = normalizeWeekdayValue(startRaw);
148
+ const end = normalizeWeekdayValue(endRaw);
149
+ if (start === null || end === null || start > end) {
150
+ return null;
151
+ }
152
+ for (let value = start; value <= end; value += 1) {
153
+ values.add(value);
154
+ }
155
+ continue;
156
+ }
157
+ const value = normalizeWeekdayValue(trimmedToken);
158
+ if (value === null) {
159
+ return null;
160
+ }
161
+ values.add(value);
162
+ }
163
+ return values;
164
+ }
165
+ function hasExactValues(values, expectedValues) {
166
+ return (values.size === expectedValues.length && expectedValues.every((value) => values.has(value)));
167
+ }
168
+ function formatCronTaskBadge(cron) {
169
+ const parts = parseCronParts(cron);
170
+ if (!parts) {
171
+ return "cron";
172
+ }
173
+ const minute = parseExactNumber(parts.minute, 0, 59);
174
+ const hour = parseExactNumber(parts.hour, 0, 23);
175
+ const everyMinuteStep = parseEveryStep(parts.minute, 1, 59);
176
+ const everyHourStep = parseEveryStep(parts.hour, 1, 23);
177
+ const monthIsWildcard = parts.month === "*";
178
+ const dayOfMonthIsWildcard = parts.dayOfMonth === "*";
179
+ const dayOfWeekIsWildcard = parts.dayOfWeek === "*";
180
+ if (everyMinuteStep !== null &&
181
+ parts.hour === "*" &&
182
+ monthIsWildcard &&
183
+ dayOfMonthIsWildcard &&
184
+ dayOfWeekIsWildcard) {
185
+ return `${everyMinuteStep}m`;
186
+ }
187
+ if (minute !== null &&
188
+ everyHourStep !== null &&
189
+ monthIsWildcard &&
190
+ dayOfMonthIsWildcard &&
191
+ dayOfWeekIsWildcard) {
192
+ return minute === 0 ? `${everyHourStep}h` : `${everyHourStep}h :${padNumber(minute)}`;
193
+ }
194
+ if (minute !== null &&
195
+ parts.hour === "*" &&
196
+ monthIsWildcard &&
197
+ dayOfMonthIsWildcard &&
198
+ dayOfWeekIsWildcard) {
199
+ return minute === 0 ? "hourly" : `hourly :${padNumber(minute)}`;
200
+ }
201
+ if (minute !== null &&
202
+ hour !== null &&
203
+ monthIsWildcard &&
204
+ dayOfMonthIsWildcard &&
205
+ dayOfWeekIsWildcard) {
206
+ return `daily ${formatTime(hour, minute)}`;
207
+ }
208
+ if (minute !== null && hour !== null && monthIsWildcard && dayOfMonthIsWildcard) {
209
+ const weekdayValues = parseWeekdaySet(parts.dayOfWeek);
210
+ if (!weekdayValues) {
211
+ return "cron";
212
+ }
213
+ if (hasExactValues(weekdayValues, [1, 2, 3, 4, 5])) {
214
+ return `weekdays ${formatTime(hour, minute)}`;
215
+ }
216
+ if (hasExactValues(weekdayValues, [0, 6])) {
217
+ return `weekends ${formatTime(hour, minute)}`;
218
+ }
219
+ if (weekdayValues.size === 1) {
220
+ const [weekday] = weekdayValues;
221
+ return `${WEEKDAY_LABELS[weekday]} ${formatTime(hour, minute)}`;
222
+ }
223
+ }
224
+ const dayOfMonth = parseExactNumber(parts.dayOfMonth, 1, 31);
225
+ if (minute !== null &&
226
+ hour !== null &&
227
+ dayOfMonth !== null &&
228
+ monthIsWildcard &&
229
+ dayOfWeekIsWildcard) {
230
+ return `monthly ${dayOfMonth} ${formatTime(hour, minute)}`;
231
+ }
232
+ return "cron";
233
+ }
234
+ export function formatTaskListBadge(task) {
235
+ if (task.kind === "once") {
236
+ return formatOnceTaskBadge(task.runAt, task.timezone);
237
+ }
238
+ return formatCronTaskBadge(task.cron);
239
+ }
@@ -0,0 +1,87 @@
1
+ import { opencodeClient } from "../opencode/client.js";
2
+ import { logger } from "../utils/logger.js";
3
+ const SCHEDULED_TASK_AGENT = "build";
4
+ const SCHEDULED_TASK_SESSION_TITLE = "Scheduled task run";
5
+ function collectResponseText(parts) {
6
+ return parts
7
+ .filter((part) => part.type === "text" && typeof part.text === "string" && !part.ignored)
8
+ .map((part) => part.text)
9
+ .join("")
10
+ .trim();
11
+ }
12
+ function toErrorMessage(error) {
13
+ if (error instanceof Error && error.message.trim()) {
14
+ return error.message.trim();
15
+ }
16
+ if (typeof error === "string" && error.trim()) {
17
+ return error.trim();
18
+ }
19
+ return "Unknown scheduled task execution error";
20
+ }
21
+ export async function executeScheduledTask(task) {
22
+ const startedAt = new Date().toISOString();
23
+ let sessionId = null;
24
+ try {
25
+ const { data: session, error: createError } = await opencodeClient.session.create({
26
+ directory: task.projectWorktree,
27
+ title: SCHEDULED_TASK_SESSION_TITLE,
28
+ });
29
+ if (createError || !session) {
30
+ throw createError || new Error("Failed to create temporary scheduled task session");
31
+ }
32
+ sessionId = session.id;
33
+ const promptOptions = {
34
+ sessionID: session.id,
35
+ directory: session.directory,
36
+ parts: [{ type: "text", text: task.prompt }],
37
+ agent: SCHEDULED_TASK_AGENT,
38
+ };
39
+ if (task.model.providerID && task.model.modelID) {
40
+ promptOptions.model = {
41
+ providerID: task.model.providerID,
42
+ modelID: task.model.modelID,
43
+ };
44
+ }
45
+ if (task.model.variant) {
46
+ promptOptions.variant = task.model.variant;
47
+ }
48
+ const { data: response, error: promptError } = await opencodeClient.session.prompt(promptOptions);
49
+ if (promptError || !response) {
50
+ throw promptError || new Error("Scheduled task prompt execution failed");
51
+ }
52
+ const resultText = collectResponseText(response.parts);
53
+ if (!resultText) {
54
+ throw new Error("Scheduled task returned an empty assistant response");
55
+ }
56
+ return {
57
+ taskId: task.id,
58
+ status: "success",
59
+ startedAt,
60
+ finishedAt: new Date().toISOString(),
61
+ resultText,
62
+ errorMessage: null,
63
+ };
64
+ }
65
+ catch (error) {
66
+ const errorMessage = toErrorMessage(error);
67
+ logger.warn(`[ScheduledTaskExecutor] Task execution failed: id=${task.id}, message=${errorMessage}`);
68
+ return {
69
+ taskId: task.id,
70
+ status: "error",
71
+ startedAt,
72
+ finishedAt: new Date().toISOString(),
73
+ resultText: null,
74
+ errorMessage,
75
+ };
76
+ }
77
+ finally {
78
+ if (sessionId) {
79
+ try {
80
+ await opencodeClient.session.delete({ sessionID: sessionId });
81
+ }
82
+ catch (error) {
83
+ logger.warn(`[ScheduledTaskExecutor] Failed to delete temporary session: sessionId=${sessionId}`, error);
84
+ }
85
+ }
86
+ }
87
+ }
@@ -0,0 +1,32 @@
1
+ import { logger } from "../utils/logger.js";
2
+ class ForegroundSessionState {
3
+ activeSessionIds = new Set();
4
+ markBusy(sessionId) {
5
+ if (!sessionId) {
6
+ return;
7
+ }
8
+ this.activeSessionIds.add(sessionId);
9
+ logger.debug(`[ScheduledTaskForeground] Marked session busy: session=${sessionId}, count=${this.activeSessionIds.size}`);
10
+ }
11
+ markIdle(sessionId) {
12
+ if (!sessionId) {
13
+ return;
14
+ }
15
+ this.activeSessionIds.delete(sessionId);
16
+ logger.debug(`[ScheduledTaskForeground] Marked session idle: session=${sessionId}, count=${this.activeSessionIds.size}`);
17
+ }
18
+ isBusy() {
19
+ return this.activeSessionIds.size > 0;
20
+ }
21
+ clearAll(reason) {
22
+ if (this.activeSessionIds.size === 0) {
23
+ return;
24
+ }
25
+ logger.info(`[ScheduledTaskForeground] Cleared foreground busy state: reason=${reason}, count=${this.activeSessionIds.size}`);
26
+ this.activeSessionIds.clear();
27
+ }
28
+ __resetForTests() {
29
+ this.activeSessionIds.clear();
30
+ }
31
+ }
32
+ export const foregroundSessionState = new ForegroundSessionState();
@@ -0,0 +1,207 @@
1
+ const MINUTE_MS = 60_000;
2
+ const MAX_SEARCH_MINUTES = 60 * 24 * 366 * 2;
3
+ const MONTH_ALIASES = {
4
+ jan: 1,
5
+ feb: 2,
6
+ mar: 3,
7
+ apr: 4,
8
+ may: 5,
9
+ jun: 6,
10
+ jul: 7,
11
+ aug: 8,
12
+ sep: 9,
13
+ oct: 10,
14
+ nov: 11,
15
+ dec: 12,
16
+ };
17
+ const WEEKDAY_ALIASES = {
18
+ sun: 0,
19
+ mon: 1,
20
+ tue: 2,
21
+ wed: 3,
22
+ thu: 4,
23
+ fri: 5,
24
+ sat: 6,
25
+ };
26
+ const zonedFormatterCache = new Map();
27
+ function getZonedFormatter(timezone) {
28
+ const cached = zonedFormatterCache.get(timezone);
29
+ if (cached) {
30
+ return cached;
31
+ }
32
+ const formatter = new Intl.DateTimeFormat("en-US", {
33
+ timeZone: timezone,
34
+ year: "numeric",
35
+ month: "numeric",
36
+ day: "numeric",
37
+ hour: "numeric",
38
+ minute: "numeric",
39
+ weekday: "short",
40
+ hour12: false,
41
+ hourCycle: "h23",
42
+ });
43
+ zonedFormatterCache.set(timezone, formatter);
44
+ return formatter;
45
+ }
46
+ function normalizeWeekday(value) {
47
+ return value === 7 ? 0 : value;
48
+ }
49
+ function parseFieldValue(rawValue, min, max, aliases) {
50
+ const normalized = rawValue.trim().toLowerCase();
51
+ if (aliases && normalized in aliases) {
52
+ return aliases[normalized];
53
+ }
54
+ const parsed = Number.parseInt(normalized, 10);
55
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
56
+ throw new Error(`Invalid cron field value: ${rawValue}`);
57
+ }
58
+ return parsed;
59
+ }
60
+ function expandFieldBase(base, min, max, aliases) {
61
+ if (base === "*") {
62
+ return Array.from({ length: max - min + 1 }, (_, index) => min + index);
63
+ }
64
+ if (base.includes("-")) {
65
+ const [startRaw, endRaw] = base.split("-");
66
+ const start = parseFieldValue(startRaw, min, max, aliases);
67
+ const end = parseFieldValue(endRaw, min, max, aliases);
68
+ if (start > end) {
69
+ throw new Error(`Invalid cron field range: ${base}`);
70
+ }
71
+ return Array.from({ length: end - start + 1 }, (_, index) => start + index);
72
+ }
73
+ return [parseFieldValue(base, min, max, aliases)];
74
+ }
75
+ function expandFieldToken(token, min, max, aliases) {
76
+ const [baseRaw, stepRaw] = token.split("/");
77
+ const baseValues = expandFieldBase(baseRaw, min, max, aliases);
78
+ if (stepRaw === undefined) {
79
+ return baseValues;
80
+ }
81
+ const step = Number.parseInt(stepRaw, 10);
82
+ if (!Number.isInteger(step) || step <= 0) {
83
+ throw new Error(`Invalid cron field step: ${token}`);
84
+ }
85
+ return baseValues.filter((value, index) => {
86
+ if (baseRaw === "*") {
87
+ return (value - min) % step === 0;
88
+ }
89
+ return index % step === 0;
90
+ });
91
+ }
92
+ function parseCronField(field, min, max, aliases, normalize) {
93
+ const normalizedField = field.trim().toLowerCase();
94
+ const values = new Set();
95
+ for (const token of normalizedField.split(",")) {
96
+ const trimmedToken = token.trim();
97
+ if (!trimmedToken) {
98
+ throw new Error(`Invalid cron field: ${field}`);
99
+ }
100
+ for (const value of expandFieldToken(trimmedToken, min, max, aliases)) {
101
+ values.add(normalize ? normalize(value) : value);
102
+ }
103
+ }
104
+ return {
105
+ wildcard: normalizedField === "*",
106
+ values,
107
+ };
108
+ }
109
+ function parseCronExpression(cron) {
110
+ const parts = cron.trim().split(/\s+/);
111
+ if (parts.length !== 5) {
112
+ throw new Error(`Unsupported cron expression: ${cron}`);
113
+ }
114
+ return {
115
+ minute: parseCronField(parts[0], 0, 59),
116
+ hour: parseCronField(parts[1], 0, 23),
117
+ dayOfMonth: parseCronField(parts[2], 1, 31),
118
+ month: parseCronField(parts[3], 1, 12, MONTH_ALIASES),
119
+ dayOfWeek: parseCronField(parts[4], 0, 7, WEEKDAY_ALIASES, normalizeWeekday),
120
+ };
121
+ }
122
+ function getZonedDateParts(date, timezone) {
123
+ const parts = getZonedFormatter(timezone).formatToParts(date);
124
+ const year = Number(parts.find((part) => part.type === "year")?.value);
125
+ const month = Number(parts.find((part) => part.type === "month")?.value);
126
+ const day = Number(parts.find((part) => part.type === "day")?.value);
127
+ const hour = Number(parts.find((part) => part.type === "hour")?.value);
128
+ const minute = Number(parts.find((part) => part.type === "minute")?.value);
129
+ const weekdayName = parts
130
+ .find((part) => part.type === "weekday")
131
+ ?.value?.toLowerCase()
132
+ .slice(0, 3);
133
+ if (!Number.isInteger(year) ||
134
+ !Number.isInteger(month) ||
135
+ !Number.isInteger(day) ||
136
+ !Number.isInteger(hour) ||
137
+ !Number.isInteger(minute) ||
138
+ !weekdayName ||
139
+ !(weekdayName in WEEKDAY_ALIASES)) {
140
+ throw new Error(`Failed to resolve zoned date parts for timezone: ${timezone}`);
141
+ }
142
+ return {
143
+ year,
144
+ month,
145
+ day,
146
+ hour,
147
+ minute,
148
+ weekday: WEEKDAY_ALIASES[weekdayName],
149
+ };
150
+ }
151
+ function matchesCronField(field, value) {
152
+ return field.values.has(value);
153
+ }
154
+ function matchesCron(expression, date, timezone) {
155
+ const parts = getZonedDateParts(date, timezone);
156
+ const minuteMatch = matchesCronField(expression.minute, parts.minute);
157
+ const hourMatch = matchesCronField(expression.hour, parts.hour);
158
+ const monthMatch = matchesCronField(expression.month, parts.month);
159
+ const dayOfMonthMatch = matchesCronField(expression.dayOfMonth, parts.day);
160
+ const dayOfWeekMatch = matchesCronField(expression.dayOfWeek, parts.weekday);
161
+ let dayMatch = false;
162
+ if (expression.dayOfMonth.wildcard && expression.dayOfWeek.wildcard) {
163
+ dayMatch = true;
164
+ }
165
+ else if (expression.dayOfMonth.wildcard) {
166
+ dayMatch = dayOfWeekMatch;
167
+ }
168
+ else if (expression.dayOfWeek.wildcard) {
169
+ dayMatch = dayOfMonthMatch;
170
+ }
171
+ else {
172
+ dayMatch = dayOfMonthMatch || dayOfWeekMatch;
173
+ }
174
+ return minuteMatch && hourMatch && monthMatch && dayMatch;
175
+ }
176
+ export function isTaskDue(task, now = new Date()) {
177
+ if (!task.nextRunAt) {
178
+ return false;
179
+ }
180
+ const nextRunAtMs = Date.parse(task.nextRunAt);
181
+ if (Number.isNaN(nextRunAtMs)) {
182
+ return false;
183
+ }
184
+ return nextRunAtMs <= now.getTime();
185
+ }
186
+ export function computeNextCronRunAt(cron, timezone, fromDate = new Date()) {
187
+ const expression = parseCronExpression(cron);
188
+ let candidateMs = Math.floor(fromDate.getTime() / MINUTE_MS) * MINUTE_MS + MINUTE_MS;
189
+ for (let attempt = 0; attempt < MAX_SEARCH_MINUTES; attempt++) {
190
+ const candidate = new Date(candidateMs);
191
+ if (matchesCron(expression, candidate, timezone)) {
192
+ return candidate.toISOString();
193
+ }
194
+ candidateMs += MINUTE_MS;
195
+ }
196
+ throw new Error(`Unable to compute next cron run for expression: ${cron}`);
197
+ }
198
+ export function computeNextRunAt(task, fromDate = new Date()) {
199
+ if (task.kind === "once") {
200
+ const runAtMs = Date.parse(task.runAt);
201
+ if (Number.isNaN(runAtMs) || runAtMs <= fromDate.getTime()) {
202
+ return null;
203
+ }
204
+ return new Date(runAtMs).toISOString();
205
+ }
206
+ return computeNextCronRunAt(task.cron, task.timezone, fromDate);
207
+ }