@smythos/sre 1.6.14 → 1.7.1

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 (81) hide show
  1. package/CHANGELOG +15 -0
  2. package/dist/index.js +52 -46
  3. package/dist/index.js.map +1 -1
  4. package/dist/types/Components/APIEndpoint.class.d.ts +2 -8
  5. package/dist/types/Components/Component.class.d.ts +9 -0
  6. package/dist/types/Components/Triggers/Gmail.trigger.d.ts +0 -17
  7. package/dist/types/Components/Triggers/JobScheduler.trigger.d.ts +10 -0
  8. package/dist/types/Components/Triggers/Trigger.class.d.ts +11 -0
  9. package/dist/types/Components/index.d.ts +6 -0
  10. package/dist/types/Core/Connector.class.d.ts +1 -0
  11. package/dist/types/Core/ConnectorsService.d.ts +2 -0
  12. package/dist/types/Core/HookService.d.ts +1 -1
  13. package/dist/types/helpers/Conversation.helper.d.ts +2 -0
  14. package/dist/types/helpers/Crypto.helper.d.ts +8 -0
  15. package/dist/types/index.d.ts +13 -0
  16. package/dist/types/subsystems/AgentManager/Agent.class.d.ts +4 -2
  17. package/dist/types/subsystems/AgentManager/AgentData.service/AgentDataConnector.d.ts +13 -0
  18. package/dist/types/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.d.ts +1 -4
  19. package/dist/types/subsystems/AgentManager/Scheduler.service/Job.class.d.ts +29 -6
  20. package/dist/types/subsystems/AgentManager/Scheduler.service/SchedulerConnector.d.ts +11 -3
  21. package/dist/types/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.d.ts +31 -7
  22. package/dist/types/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.d.ts +2 -5
  23. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.d.ts +3 -6
  24. package/dist/types/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.d.ts +7 -0
  25. package/dist/types/subsystems/LLMManager/LLM.service/connectors/xAI.class.d.ts +2 -5
  26. package/dist/types/types/Agent.types.d.ts +1 -0
  27. package/dist/types/types/LLM.types.d.ts +2 -5
  28. package/dist/types/types/SRE.types.d.ts +4 -1
  29. package/package.json +6 -2
  30. package/src/Components/APICall/OAuth.helper.ts +30 -35
  31. package/src/Components/APIEndpoint.class.ts +25 -6
  32. package/src/Components/Component.class.ts +11 -0
  33. package/src/Components/Triggers/Gmail.trigger.ts +282 -0
  34. package/src/Components/Triggers/JobScheduler.trigger.ts +45 -0
  35. package/src/Components/Triggers/README.md +3 -0
  36. package/src/Components/Triggers/Trigger.class.ts +101 -0
  37. package/src/Components/Triggers/WhatsApp.trigger.ts +219 -0
  38. package/src/Components/index.ts +8 -0
  39. package/src/Core/AgentProcess.helper.ts +4 -6
  40. package/src/Core/Connector.class.ts +11 -3
  41. package/src/Core/ConnectorsService.ts +5 -0
  42. package/src/Core/ExternalEventsReceiver.ts +317 -0
  43. package/src/Core/HookService.ts +20 -6
  44. package/src/Core/SmythRuntime.class.ts +7 -0
  45. package/src/Core/SystemEvents.ts +17 -0
  46. package/src/Core/boot.ts +2 -0
  47. package/src/helpers/Conversation.helper.ts +35 -11
  48. package/src/helpers/Crypto.helper.ts +28 -0
  49. package/src/index.ts +208 -195
  50. package/src/index.ts.bak +208 -195
  51. package/src/subsystems/AGENTS.md +594 -0
  52. package/src/subsystems/AgentManager/Agent.class.ts +71 -21
  53. package/src/subsystems/AgentManager/AgentData.service/AgentDataConnector.ts +24 -1
  54. package/src/subsystems/AgentManager/AgentData.service/connectors/NullAgentData.class.ts +2 -2
  55. package/src/subsystems/AgentManager/AgentRuntime.class.ts +34 -5
  56. package/src/subsystems/AgentManager/Scheduler.service/Job.class.ts +414 -0
  57. package/src/subsystems/AgentManager/Scheduler.service/Schedule.class.ts +200 -0
  58. package/src/subsystems/AgentManager/Scheduler.service/SchedulerConnector.ts +200 -0
  59. package/src/subsystems/AgentManager/Scheduler.service/connectors/LocalScheduler.class.ts +767 -0
  60. package/src/subsystems/AgentManager/Scheduler.service/index.ts +11 -0
  61. package/src/subsystems/IO/VectorDB.service/connectors/MilvusVectorDB.class.ts +1 -1
  62. package/src/subsystems/LLMManager/LLM.service/LLMCredentials.helper.ts +61 -2
  63. package/src/subsystems/LLMManager/LLM.service/connectors/Anthropic.class.ts +3 -0
  64. package/src/subsystems/LLMManager/LLM.service/connectors/Bedrock.class.ts +3 -1
  65. package/src/subsystems/LLMManager/LLM.service/connectors/Echo.class.ts +5 -1
  66. package/src/subsystems/LLMManager/LLM.service/connectors/GoogleAI.class.ts +247 -56
  67. package/src/subsystems/LLMManager/LLM.service/connectors/Groq.class.ts +3 -0
  68. package/src/subsystems/LLMManager/LLM.service/connectors/Ollama.class.ts +28 -21
  69. package/src/subsystems/LLMManager/LLM.service/connectors/Perplexity.class.ts +3 -0
  70. package/src/subsystems/LLMManager/LLM.service/connectors/VertexAI.class.ts +121 -33
  71. package/src/subsystems/LLMManager/LLM.service/connectors/openai/OpenAIConnector.class.ts +38 -27
  72. package/src/subsystems/LLMManager/LLM.service/connectors/openai/apiInterfaces/ResponsesApiInterface.ts +115 -18
  73. package/src/subsystems/LLMManager/LLM.service/connectors/xAI.class.ts +3 -0
  74. package/src/subsystems/LLMManager/ModelsProvider.service/ModelsProviderConnector.ts +1 -6
  75. package/src/subsystems/MemoryManager/LLMContext.ts +3 -8
  76. package/src/subsystems/MemoryManager/RuntimeContext.ts +10 -9
  77. package/src/subsystems/Security/Credentials/Credentials.class.ts +1 -0
  78. package/src/subsystems/Security/Credentials/ManagedOAuth2Credentials.class.ts +106 -0
  79. package/src/types/Agent.types.ts +1 -0
  80. package/src/types/LLM.types.ts +2 -2
  81. package/src/types/SRE.types.ts +3 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Job - Wrapper for agent-based scheduled tasks
3
+ *
4
+ * Jobs can execute in three ways:
5
+ * 1. **Skill Execution**: Call a specific agent skill with arguments
6
+ * 2. **Trigger Execution**: Invoke an agent trigger (no arguments)
7
+ * 3. **Prompt Execution**: Send a prompt to an agent for processing
8
+ *
9
+ * All job data is fully serializable, allowing jobs to persist and resume after restart.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * // Skill execution job
14
+ * const skillJob = new Job({
15
+ * type: 'skill',
16
+ * agentId: 'my-agent',
17
+ * skillName: 'process_data',
18
+ * args: { input: 'test data' },
19
+ * metadata: {
20
+ * name: 'Data Processing',
21
+ * description: 'Process data every hour',
22
+ * retryOnFailure: true,
23
+ * maxRetries: 3
24
+ * }
25
+ * });
26
+ *
27
+ * // Trigger execution job
28
+ * const triggerJob = new Job({
29
+ * type: 'trigger',
30
+ * agentId: 'my-agent',
31
+ * triggerName: 'daily_sync',
32
+ * metadata: {
33
+ * name: 'Daily Sync',
34
+ * description: 'Sync data every day at midnight'
35
+ * }
36
+ * });
37
+ *
38
+ * // Prompt execution job
39
+ * const promptJob = new Job({
40
+ * type: 'prompt',
41
+ * agentId: 'my-agent',
42
+ * prompt: 'Generate daily report',
43
+ * metadata: {
44
+ * name: 'Daily Report',
45
+ * description: 'Generate report every day'
46
+ * }
47
+ * });
48
+ * ```
49
+ */
50
+
51
+ import { AgentProcess } from '@sre/Core/AgentProcess.helper';
52
+ import { Conversation } from '@sre/helpers/Conversation.helper';
53
+ import { ConnectorService } from '@sre/Core/ConnectorsService';
54
+ import { hookAsync, HookService } from '@sre/Core/HookService';
55
+ import { Logger } from '@sre/helpers/Log.helper';
56
+ const console = Logger('Scheduler/Job');
57
+ export interface IJobMetadata {
58
+ name?: string;
59
+ description?: string;
60
+ tags?: string[];
61
+ retryOnFailure?: boolean;
62
+ maxRetries?: number;
63
+ timeout?: number; // in milliseconds
64
+ [key: string]: any; // Additional custom metadata
65
+ }
66
+
67
+ /**
68
+ * Configuration for a skill-based job
69
+ */
70
+ export interface ISkillJobConfig {
71
+ type: 'skill';
72
+ agentId: string;
73
+ skillName: string;
74
+ args?: Record<string, any> | any[];
75
+ metadata?: IJobMetadata;
76
+ }
77
+
78
+ export interface ITriggerJobConfig {
79
+ type: 'trigger';
80
+ agentId: string;
81
+ triggerName: string;
82
+ metadata?: IJobMetadata;
83
+ }
84
+
85
+ /**
86
+ * Configuration for a prompt-based job
87
+ */
88
+ export interface IPromptJobConfig {
89
+ type: 'prompt';
90
+ agentId: string;
91
+ prompt: string;
92
+ metadata?: IJobMetadata;
93
+ }
94
+
95
+ export type IJobConfig = ISkillJobConfig | IPromptJobConfig | ITriggerJobConfig;
96
+
97
+ export class Job {
98
+ private config: IJobConfig;
99
+ public get agentId(): string {
100
+ return this.config.agentId;
101
+ }
102
+
103
+ constructor(config: IJobConfig) {
104
+ // Validate configuration
105
+ if (!config.type || (config.type !== 'skill' && config.type !== 'prompt' && config.type !== 'trigger')) {
106
+ throw new Error('Job type must be either "skill", "prompt", or "trigger"');
107
+ }
108
+ if (!config.agentId) {
109
+ throw new Error('Job must have an agentId');
110
+ }
111
+ if (config.type === 'skill' && !(config as ISkillJobConfig).skillName) {
112
+ throw new Error('Skill job must have a skillName');
113
+ }
114
+ if (config.type === 'prompt' && !(config as IPromptJobConfig).prompt) {
115
+ throw new Error('Prompt job must have a prompt');
116
+ }
117
+ if (config.type === 'trigger' && !(config as ITriggerJobConfig).triggerName) {
118
+ throw new Error('Trigger job must have a triggerName');
119
+ }
120
+
121
+ this.config = {
122
+ ...config,
123
+ metadata: {
124
+ retryOnFailure: false,
125
+ maxRetries: 0,
126
+ tags: [],
127
+ ...config.metadata,
128
+ },
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Get the job metadata
134
+ */
135
+ public getMetadata(): IJobMetadata {
136
+ return { ...this.config.metadata };
137
+ }
138
+
139
+ /**
140
+ * Get the job configuration
141
+ */
142
+ public getConfig(): IJobConfig {
143
+ return { ...this.config };
144
+ }
145
+
146
+ /**
147
+ * Execute the job with error handling and timeout support
148
+ * @returns Execution result
149
+ */
150
+ @hookAsync('Scheduler/Job.execute')
151
+ public async execute(): Promise<{ success: boolean; error?: Error; executionTime: number; result?: any }> {
152
+ const startTime = Date.now();
153
+
154
+ try {
155
+ let result: any;
156
+
157
+ // Execute with timeout if specified
158
+ if (this.config.metadata.timeout && this.config.metadata.timeout > 0) {
159
+ result = await this.executeWithTimeout(this.config.metadata.timeout);
160
+ } else {
161
+ result = await this.executeInternal();
162
+ }
163
+
164
+ const executionTime = Date.now() - startTime;
165
+ return { success: true, executionTime, result };
166
+ } catch (error) {
167
+ const executionTime = Date.now() - startTime;
168
+ return {
169
+ success: false,
170
+ error: error instanceof Error ? error : new Error(String(error)),
171
+ executionTime,
172
+ };
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Internal execution logic based on job type
178
+ */
179
+ private async executeInternal(): Promise<any> {
180
+ if (this.config.type === 'skill') {
181
+ return await this.executeSkill();
182
+ } else if (this.config.type === 'trigger') {
183
+ return await this.executeTrigger();
184
+ } else {
185
+ return await this.executePrompt();
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Execute a skill-based job
191
+ */
192
+ private async executeSkill(): Promise<any> {
193
+ const config = this.config as ISkillJobConfig;
194
+
195
+ // Get agent data
196
+ const agentDataConnector = ConnectorService.getAgentDataConnector();
197
+ const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
198
+
199
+ if (!agentData) {
200
+ console.debug('Job execusion skiped, agent not found: ', config.agentId);
201
+ throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
202
+ }
203
+
204
+ // Handle different agent data structures
205
+ // AgentData can be: { data: { components, connections }, version } or { components, connections }
206
+ const actualData = agentData.data || agentData;
207
+ const components = actualData.components;
208
+
209
+ if (!components || !Array.isArray(components)) {
210
+ console.debug('Job execusion skiped, agent not found: ', config.agentId);
211
+
212
+ throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
213
+ }
214
+
215
+ // Find the skill in agent data
216
+ const skill = components.find((c: any) => {
217
+ const endpoint = c.data?.endpoint || c.endpoint;
218
+ return endpoint === config.skillName;
219
+ });
220
+
221
+ if (!skill) {
222
+ throw new Error(`Skill ${config.skillName} not found in agent ${config.agentId}`);
223
+ }
224
+
225
+ // Prepare request based on skill method
226
+ const method = (skill.data?.method || skill.method || 'POST').toUpperCase();
227
+ const path = `/api/${config.skillName}`;
228
+ const headers = {
229
+ 'Content-Type': 'application/json',
230
+ };
231
+
232
+ const args = config.args || {};
233
+ const body = method === 'POST' ? args : undefined;
234
+ const query = method === 'GET' ? args : undefined;
235
+
236
+ // Load agent and execute
237
+ const agent = AgentProcess.load(agentData);
238
+ await agent.ready();
239
+
240
+ const result = await agent.run({ method, path, body, query, headers });
241
+ return result.data;
242
+ }
243
+
244
+ /**
245
+ * Execute a trigger-based job
246
+ */
247
+ private async executeTrigger(): Promise<any> {
248
+ const config = this.config as ITriggerJobConfig;
249
+
250
+ // Get agent data
251
+ const agentDataConnector = ConnectorService.getAgentDataConnector();
252
+ const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
253
+
254
+ if (!agentData) {
255
+ console.debug('Job execusion skiped, agent not found: ', config.agentId);
256
+ throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
257
+ }
258
+
259
+ // Handle different agent data structures
260
+ const actualData = agentData.data || agentData;
261
+ const components = actualData.components;
262
+
263
+ if (!components || !Array.isArray(components)) {
264
+ console.debug('Job execusion skiped, agent not found: ', config.agentId);
265
+ throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
266
+ }
267
+
268
+ // Find the trigger in agent data (triggers have triggerEndpoint property)
269
+ const trigger = components.find((c: any) => {
270
+ const triggerEndpoint = c.data?.triggerEndpoint || c.triggerEndpoint;
271
+ return triggerEndpoint === config.triggerName;
272
+ });
273
+
274
+ if (!trigger) {
275
+ throw new Error(`Trigger ${config.triggerName} not found in agent ${config.agentId}`);
276
+ }
277
+
278
+ // Prepare request for trigger
279
+ // Triggers don't use HTTP methods like skills, they're just invoked via path
280
+ const path = `/trigger/${config.triggerName}`;
281
+ const headers = {
282
+ 'Content-Type': 'application/json',
283
+ };
284
+
285
+ // Load agent and execute trigger (triggers don't take arguments)
286
+ const agent = AgentProcess.load(agentData);
287
+ await agent.ready();
288
+
289
+ const result = await agent.run({ method: 'POST', path, body: {}, query: {}, headers });
290
+ return result.data;
291
+ }
292
+
293
+ /**
294
+ * Execute a prompt-based job
295
+ */
296
+ private async executePrompt(): Promise<any> {
297
+ const config = this.config as IPromptJobConfig;
298
+
299
+ // Get agent data
300
+ const agentDataConnector = ConnectorService.getAgentDataConnector();
301
+ const agentData = (await agentDataConnector.getEphemeralAgentData(config.agentId)) || (await agentDataConnector.getAgentData(config.agentId));
302
+
303
+ if (!agentData) {
304
+ console.debug('Job execusion skiped, agent not found: ', config.agentId);
305
+ throw new Error(`Job execution skipped, agent not found: ${config.agentId}`);
306
+ }
307
+
308
+ // Handle different agent data structures
309
+ const actualData = agentData.data || agentData;
310
+ const defaultModel = actualData.defaultModel;
311
+ const behavior = actualData.behavior || '';
312
+
313
+ if (!defaultModel) {
314
+ throw new Error(`No model configured for agent ${config.agentId}`);
315
+ }
316
+
317
+ // Create conversation with agent
318
+ const conversation = new Conversation(
319
+ defaultModel,
320
+ agentData, // spec source (agent data)
321
+ {
322
+ systemPrompt: behavior,
323
+ agentId: config.agentId,
324
+ }
325
+ );
326
+
327
+ await conversation.ready;
328
+
329
+ // Execute prompt
330
+ const result = await conversation.prompt(config.prompt);
331
+ return result;
332
+ }
333
+
334
+ /**
335
+ * Execute the job with retry logic
336
+ * @param retryCount - Current retry attempt
337
+ * @returns Execution result
338
+ */
339
+ public async executeWithRetry(
340
+ retryCount: number = 0
341
+ ): Promise<{ success: boolean; error?: Error; executionTime: number; retries: number; result?: any }> {
342
+ let lastError: Error | undefined;
343
+ let totalExecutionTime = 0;
344
+ let lastResult: any;
345
+ const maxRetries = this.config.metadata.retryOnFailure ? this.config.metadata.maxRetries || 0 : 0;
346
+
347
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
348
+ const result = await this.execute();
349
+ totalExecutionTime += result.executionTime;
350
+ lastResult = result.result;
351
+
352
+ if (result.success) {
353
+ return { success: true, executionTime: totalExecutionTime, retries: attempt, result: lastResult };
354
+ }
355
+
356
+ lastError = result.error;
357
+
358
+ // Don't retry on the last attempt
359
+ if (attempt < maxRetries) {
360
+ // Wait before retrying (exponential backoff)
361
+ await this.sleep(Math.min(1000 * Math.pow(2, attempt), 30000));
362
+ }
363
+ }
364
+
365
+ return {
366
+ success: false,
367
+ error: lastError,
368
+ executionTime: totalExecutionTime,
369
+ retries: maxRetries,
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Execute with timeout
375
+ */
376
+ private async executeWithTimeout(timeoutMs: number): Promise<any> {
377
+ return new Promise((resolve, reject) => {
378
+ const timer = setTimeout(() => {
379
+ reject(new Error(`Job execution timed out after ${timeoutMs}ms`));
380
+ }, timeoutMs);
381
+
382
+ this.executeInternal()
383
+ .then((result) => {
384
+ clearTimeout(timer);
385
+ resolve(result);
386
+ })
387
+ .catch((error) => {
388
+ clearTimeout(timer);
389
+ reject(error);
390
+ });
391
+ });
392
+ }
393
+
394
+ /**
395
+ * Sleep utility for retry delays
396
+ */
397
+ private sleep(ms: number): Promise<void> {
398
+ return new Promise((resolve) => setTimeout(resolve, ms));
399
+ }
400
+
401
+ /**
402
+ * Serialize job to JSON (everything is serializable now!)
403
+ */
404
+ public toJSON(): IJobConfig {
405
+ return { ...this.config };
406
+ }
407
+
408
+ /**
409
+ * Create a Job instance from JSON
410
+ */
411
+ public static fromJSON(config: IJobConfig): Job {
412
+ return new Job(config);
413
+ }
414
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Schedule - Fluent API for building schedule definitions
3
+ *
4
+ * Supports interval-based scheduling and cron expressions.
5
+ * Serializable to JSON for persistence.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // Interval-based scheduling
10
+ * const schedule = Schedule.every('10m');
11
+ * const schedule2 = Schedule.every('30s').starts(new Date('2025-01-01')).ends(new Date('2025-12-31'));
12
+ *
13
+ * // Cron-based scheduling
14
+ * const schedule3 = Schedule.cron('0 0 * * *'); // Daily at midnight
15
+ *
16
+ * // Serialization
17
+ * const json = schedule.toJSON();
18
+ * const restored = Schedule.fromJSON(json);
19
+ * ```
20
+ */
21
+
22
+ export interface IScheduleData {
23
+ interval?: string; // e.g., "10m", "30s", "2h"
24
+ startDate?: string; // ISO 8601
25
+ endDate?: string; // ISO 8601
26
+ cron?: string; // Cron expression
27
+ }
28
+
29
+ export class Schedule {
30
+ private data: IScheduleData = {};
31
+
32
+ private constructor(data?: IScheduleData) {
33
+ if (data) {
34
+ this.data = { ...data };
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Create a schedule with an interval
40
+ * @param interval - Time interval (e.g., "10m", "30s", "2h", "1d")
41
+ */
42
+ public static every(interval: string): Schedule {
43
+ const schedule = new Schedule();
44
+ schedule.data.interval = interval;
45
+ return schedule;
46
+ }
47
+
48
+ /**
49
+ * Create a schedule with a cron expression
50
+ * @param cronExpression - Cron expression (e.g., "0 0 * * *")
51
+ */
52
+ public static cron(cronExpression: string): Schedule {
53
+ const schedule = new Schedule();
54
+ schedule.data.cron = cronExpression;
55
+ return schedule;
56
+ }
57
+
58
+ /**
59
+ * Set the start date for the schedule
60
+ * @param date - Start date
61
+ */
62
+ public starts(date: Date): Schedule {
63
+ this.data.startDate = date.toISOString();
64
+ return this;
65
+ }
66
+
67
+ /**
68
+ * Set the end date for the schedule
69
+ * @param date - End date
70
+ */
71
+ public ends(date: Date): Schedule {
72
+ this.data.endDate = date.toISOString();
73
+ return this;
74
+ }
75
+
76
+ /**
77
+ * Convert schedule to JSON representation
78
+ */
79
+ public toJSON(): IScheduleData {
80
+ return { ...this.data };
81
+ }
82
+
83
+ /**
84
+ * Create a Schedule instance from JSON data
85
+ * @param json - Schedule data
86
+ */
87
+ public static fromJSON(json: IScheduleData): Schedule {
88
+ return new Schedule(json);
89
+ }
90
+
91
+ /**
92
+ * Get the schedule data
93
+ */
94
+ public getData(): IScheduleData {
95
+ return { ...this.data };
96
+ }
97
+
98
+ /**
99
+ * Validate the schedule configuration
100
+ */
101
+ public validate(): { valid: boolean; error?: string } {
102
+ // Must have either interval or cron
103
+ if (!this.data.interval && !this.data.cron) {
104
+ return { valid: false, error: 'Schedule must have either interval or cron expression' };
105
+ }
106
+
107
+ // Cannot have both
108
+ if (this.data.interval && this.data.cron) {
109
+ return { valid: false, error: 'Schedule cannot have both interval and cron expression' };
110
+ }
111
+
112
+ // Validate interval format
113
+ if (this.data.interval) {
114
+ const intervalRegex = /^(\d+)(s|m|h|d|w)$/;
115
+ if (!intervalRegex.test(this.data.interval)) {
116
+ return { valid: false, error: 'Invalid interval format. Use format like "10m", "30s", "2h"' };
117
+ }
118
+ }
119
+
120
+ // Validate date range
121
+ if (this.data.startDate && this.data.endDate) {
122
+ const start = new Date(this.data.startDate);
123
+ const end = new Date(this.data.endDate);
124
+ if (start >= end) {
125
+ return { valid: false, error: 'Start date must be before end date' };
126
+ }
127
+ }
128
+
129
+ return { valid: true };
130
+ }
131
+
132
+ /**
133
+ * Parse interval string to milliseconds
134
+ * @param interval - Interval string (e.g., "10m")
135
+ */
136
+ public static parseInterval(interval: string): number {
137
+ const match = interval.match(/^(\d+)(s|m|h|d|w)$/);
138
+ if (!match) {
139
+ throw new Error(`Invalid interval format: ${interval}`);
140
+ }
141
+
142
+ const value = parseInt(match[1], 10);
143
+ const unit = match[2];
144
+
145
+ const multipliers: Record<string, number> = {
146
+ s: 1000, // seconds
147
+ m: 60 * 1000, // minutes
148
+ h: 60 * 60 * 1000, // hours
149
+ d: 24 * 60 * 60 * 1000, // days
150
+ w: 7 * 24 * 60 * 60 * 1000, // weeks
151
+ };
152
+
153
+ return value * multipliers[unit];
154
+ }
155
+
156
+ /**
157
+ * Check if schedule should run at the given time
158
+ * @param now - Current time
159
+ */
160
+ public shouldRun(now: Date = new Date()): boolean {
161
+ // Check date range
162
+ if (this.data.startDate && now < new Date(this.data.startDate)) {
163
+ return false;
164
+ }
165
+ if (this.data.endDate && now > new Date(this.data.endDate)) {
166
+ return false;
167
+ }
168
+ return true;
169
+ }
170
+
171
+ /**
172
+ * Calculate next run time based on last run
173
+ * @param lastRun - Last execution time
174
+ */
175
+ public calculateNextRun(lastRun?: Date): Date | null {
176
+ const now = new Date();
177
+
178
+ // If schedule has ended, no next run
179
+ if (this.data.endDate && now > new Date(this.data.endDate)) {
180
+ return null;
181
+ }
182
+
183
+ // For interval-based scheduling
184
+ if (this.data.interval) {
185
+ const intervalMs = Schedule.parseInterval(this.data.interval);
186
+ const nextRun = lastRun ? new Date(lastRun.getTime() + intervalMs) : now;
187
+
188
+ // Ensure next run is not before start date
189
+ if (this.data.startDate) {
190
+ const startDate = new Date(this.data.startDate);
191
+ return nextRun < startDate ? startDate : nextRun;
192
+ }
193
+
194
+ return nextRun;
195
+ }
196
+
197
+ // For cron-based scheduling, return null (will be handled by node-cron)
198
+ return null;
199
+ }
200
+ }