@cordbot/agent 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.
@@ -0,0 +1,62 @@
1
+ import yaml from 'js-yaml';
2
+ import fs from 'fs';
3
+ export function parseCronFile(filePath) {
4
+ if (!fs.existsSync(filePath)) {
5
+ return { jobs: [] };
6
+ }
7
+ const content = fs.readFileSync(filePath, 'utf-8');
8
+ // Handle empty file
9
+ if (!content.trim()) {
10
+ return { jobs: [] };
11
+ }
12
+ try {
13
+ const parsed = yaml.load(content);
14
+ // Validate structure
15
+ if (!parsed || typeof parsed !== 'object') {
16
+ throw new Error('Invalid YAML structure');
17
+ }
18
+ if (!Array.isArray(parsed.jobs)) {
19
+ throw new Error('jobs must be an array');
20
+ }
21
+ // Validate each job
22
+ const jobs = parsed.jobs.map((job, index) => {
23
+ if (!job.name || typeof job.name !== 'string') {
24
+ throw new Error(`Job at index ${index} is missing required field: name`);
25
+ }
26
+ if (!job.schedule || typeof job.schedule !== 'string') {
27
+ throw new Error(`Job at index ${index} is missing required field: schedule`);
28
+ }
29
+ if (!job.task || typeof job.task !== 'string') {
30
+ throw new Error(`Job at index ${index} is missing required field: task`);
31
+ }
32
+ return {
33
+ name: job.name,
34
+ schedule: job.schedule,
35
+ task: job.task,
36
+ oneTime: job.oneTime === true, // Optional field, defaults to false
37
+ };
38
+ });
39
+ return { jobs };
40
+ }
41
+ catch (error) {
42
+ if (error instanceof Error) {
43
+ throw new Error(`Failed to parse cron file: ${error.message}`);
44
+ }
45
+ throw new Error('Failed to parse cron file');
46
+ }
47
+ }
48
+ export function validateCronSchedule(schedule) {
49
+ // Basic cron format validation: "* * * * *" (minute hour day month weekday)
50
+ const parts = schedule.trim().split(/\s+/);
51
+ if (parts.length !== 5) {
52
+ return false;
53
+ }
54
+ // Each part should be either:
55
+ // - A number
56
+ // - An asterisk (*)
57
+ // - A range (e.g., 1-5)
58
+ // - A list (e.g., 1,3,5)
59
+ // - A step (e.g., */2)
60
+ const cronPartRegex = /^(\*|(\d+(-\d+)?(,\d+(-\d+)?)*)(\/\d+)?)$/;
61
+ return parts.every(part => cronPartRegex.test(part));
62
+ }
@@ -0,0 +1,201 @@
1
+ import cron from 'node-cron';
2
+ import chokidar from 'chokidar';
3
+ import fs from 'fs';
4
+ import yaml from 'js-yaml';
5
+ import { parseCronFile, validateCronSchedule } from './parser.js';
6
+ import { streamToDiscord } from '../agent/stream.js';
7
+ export class CronRunner {
8
+ client;
9
+ sessionManager;
10
+ scheduledTasks = new Map();
11
+ watchers = new Map();
12
+ channelMappings = new Map();
13
+ constructor(client, sessionManager) {
14
+ this.client = client;
15
+ this.sessionManager = sessionManager;
16
+ }
17
+ /**
18
+ * Start watching and scheduling cron jobs for all channels
19
+ */
20
+ start(channelMappings) {
21
+ console.log('⏰ Starting cron scheduler...');
22
+ for (const mapping of channelMappings) {
23
+ this.channelMappings.set(mapping.channelId, mapping);
24
+ this.watchCronFile(mapping);
25
+ }
26
+ console.log(`✅ Watching ${channelMappings.length} cron files`);
27
+ }
28
+ /**
29
+ * Add a new channel to watch
30
+ */
31
+ addChannel(mapping) {
32
+ this.channelMappings.set(mapping.channelId, mapping);
33
+ this.watchCronFile(mapping);
34
+ console.log(`✅ Now watching cron file for #${mapping.channelName}`);
35
+ }
36
+ /**
37
+ * Remove a channel from watching
38
+ */
39
+ removeChannel(channelId) {
40
+ // Stop the watcher
41
+ const watcher = this.watchers.get(channelId);
42
+ if (watcher) {
43
+ watcher.close();
44
+ this.watchers.delete(channelId);
45
+ }
46
+ // Stop all scheduled tasks for this channel
47
+ this.stopChannelTasks(channelId);
48
+ // Remove from channel mappings
49
+ this.channelMappings.delete(channelId);
50
+ console.log(`⏸️ Stopped watching channel ${channelId}`);
51
+ }
52
+ /**
53
+ * Watch a cron file for changes and schedule jobs
54
+ */
55
+ watchCronFile(mapping) {
56
+ const { cronPath, channelId, folderPath } = mapping;
57
+ // Initial load
58
+ this.loadAndScheduleJobs(channelId, cronPath, folderPath);
59
+ // Watch for changes
60
+ const watcher = chokidar.watch(cronPath, {
61
+ persistent: true,
62
+ ignoreInitial: true,
63
+ });
64
+ watcher.on('change', () => {
65
+ console.log(`🔄 Cron file changed: ${cronPath}`);
66
+ this.loadAndScheduleJobs(channelId, cronPath, folderPath);
67
+ });
68
+ watcher.on('error', (error) => {
69
+ console.error(`Failed to watch cron file ${cronPath}:`, error);
70
+ });
71
+ this.watchers.set(channelId, watcher);
72
+ }
73
+ /**
74
+ * Load cron file and schedule jobs
75
+ */
76
+ loadAndScheduleJobs(channelId, cronPath, folderPath) {
77
+ // Stop existing tasks for this channel
78
+ this.stopChannelTasks(channelId);
79
+ try {
80
+ // Parse cron file
81
+ const config = parseCronFile(cronPath);
82
+ if (config.jobs.length === 0) {
83
+ console.log(`No jobs configured for channel ${channelId}`);
84
+ return;
85
+ }
86
+ // Schedule each job
87
+ const tasks = [];
88
+ for (const job of config.jobs) {
89
+ // Validate cron schedule
90
+ if (!validateCronSchedule(job.schedule)) {
91
+ console.error(`Invalid cron schedule for job "${job.name}": ${job.schedule}`);
92
+ continue;
93
+ }
94
+ // Schedule the job
95
+ const task = cron.schedule(job.schedule, async () => {
96
+ await this.executeJob(job, channelId, folderPath);
97
+ });
98
+ tasks.push({ job, task, channelId, folderPath });
99
+ console.log(`📅 Scheduled job "${job.name}" for channel ${channelId}: ${job.schedule}`);
100
+ }
101
+ this.scheduledTasks.set(channelId, tasks);
102
+ }
103
+ catch (error) {
104
+ console.error(`Failed to load cron file ${cronPath}:`, error);
105
+ }
106
+ }
107
+ /**
108
+ * Execute a scheduled job
109
+ */
110
+ async executeJob(job, channelId, folderPath) {
111
+ console.log(`⏰ Executing scheduled job: ${job.name}`);
112
+ try {
113
+ const channel = this.client.channels.cache.get(channelId);
114
+ if (!channel) {
115
+ console.error(`Channel ${channelId} not found`);
116
+ return;
117
+ }
118
+ // Create a session for this job
119
+ const sessionId = `cron_${Date.now()}_${job.name}`;
120
+ // Set channel and working directory context for tools
121
+ this.sessionManager.setChannelContext(sessionId, channel);
122
+ this.sessionManager.setWorkingDirContext(sessionId, folderPath);
123
+ try {
124
+ // Create query for Claude
125
+ const queryResult = this.sessionManager.createQuery(job.task, null, // New session for each cron job
126
+ folderPath);
127
+ // Stream response with trigger message as prefix
128
+ await streamToDiscord(queryResult, channel, this.sessionManager, sessionId, folderPath, `⏰ **Scheduled task:** ${job.task}`);
129
+ console.log(`✅ Completed scheduled job: ${job.name}`);
130
+ // Store the session so the next message in this channel can continue it
131
+ this.sessionManager.setPendingCronSession(channelId, sessionId, folderPath);
132
+ // If this is a one-time job, remove it from the cron file
133
+ if (job.oneTime) {
134
+ await this.removeOneTimeJob(channelId, job.name, folderPath);
135
+ }
136
+ }
137
+ finally {
138
+ // Clear contexts after execution
139
+ this.sessionManager.clearChannelContext(sessionId);
140
+ this.sessionManager.clearWorkingDirContext(sessionId);
141
+ }
142
+ }
143
+ catch (error) {
144
+ console.error(`Failed to execute job "${job.name}":`, error);
145
+ }
146
+ }
147
+ /**
148
+ * Stop all tasks for a channel
149
+ */
150
+ stopChannelTasks(channelId) {
151
+ const tasks = this.scheduledTasks.get(channelId);
152
+ if (tasks) {
153
+ for (const { task, job } of tasks) {
154
+ task.stop();
155
+ console.log(`⏸️ Stopped job: ${job.name}`);
156
+ }
157
+ this.scheduledTasks.delete(channelId);
158
+ }
159
+ }
160
+ /**
161
+ * Remove a one-time job after it executes
162
+ */
163
+ async removeOneTimeJob(channelId, jobName, folderPath) {
164
+ try {
165
+ const mapping = this.channelMappings.get(channelId);
166
+ if (!mapping) {
167
+ console.error(`Cannot find channel mapping for ${channelId}`);
168
+ return;
169
+ }
170
+ const cronPath = mapping.cronPath;
171
+ // Read and parse the cron file
172
+ const config = parseCronFile(cronPath);
173
+ // Filter out the completed one-time job
174
+ const updatedJobs = config.jobs.filter(job => job.name !== jobName);
175
+ // Write back to file
176
+ const yamlContent = yaml.dump({ jobs: updatedJobs });
177
+ fs.writeFileSync(cronPath, yamlContent, 'utf-8');
178
+ console.log(`🗑️ Removed one-time job: ${jobName}`);
179
+ }
180
+ catch (error) {
181
+ console.error(`Failed to remove one-time job "${jobName}":`, error);
182
+ }
183
+ }
184
+ /**
185
+ * Stop all scheduled tasks and watchers
186
+ */
187
+ stop() {
188
+ console.log('⏸️ Stopping cron scheduler...');
189
+ // Stop all scheduled tasks
190
+ for (const [channelId, tasks] of this.scheduledTasks) {
191
+ this.stopChannelTasks(channelId);
192
+ }
193
+ // Stop all watchers
194
+ for (const [channelId, watcher] of this.watchers) {
195
+ watcher.close();
196
+ console.log(`⏸️ Stopped watching channel ${channelId}`);
197
+ }
198
+ this.watchers.clear();
199
+ console.log('✅ Cron scheduler stopped');
200
+ }
201
+ }
@@ -0,0 +1,2 @@
1
+ // Firebase Cloud Functions URL (not the hosting URL)
2
+ export const SERVICE_URL = process.env.SERVICE_URL || 'https://us-central1-claudebot-34c42.cloudfunctions.net';
@@ -0,0 +1,27 @@
1
+ export async function fetchManifest(botToken, serviceUrl) {
2
+ try {
3
+ const response = await fetch(`${serviceUrl}/getBotManifest`, {
4
+ method: 'POST',
5
+ headers: {
6
+ 'Content-Type': 'application/json',
7
+ },
8
+ body: JSON.stringify({
9
+ data: { botToken },
10
+ }),
11
+ });
12
+ if (!response.ok) {
13
+ console.error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
14
+ return null;
15
+ }
16
+ const data = await response.json();
17
+ if (data.result?.error) {
18
+ console.error(`Manifest error: ${data.result.error}`);
19
+ return null;
20
+ }
21
+ return data.result;
22
+ }
23
+ catch (error) {
24
+ console.error('Error fetching manifest:', error);
25
+ return null;
26
+ }
27
+ }
@@ -0,0 +1,143 @@
1
+ import { fetchManifest } from './manifest.js';
2
+ /**
3
+ * TokenManager handles OAuth token lifecycle:
4
+ * - Provides fresh tokens to tools
5
+ * - Automatically refreshes tokens before expiry
6
+ * - Runs background refresh loop
7
+ */
8
+ export class TokenManager {
9
+ botToken;
10
+ serviceUrl;
11
+ manifest = null;
12
+ refreshInterval = null;
13
+ isRefreshing = false;
14
+ constructor(botToken, serviceUrl, initialManifest = null) {
15
+ this.botToken = botToken;
16
+ this.serviceUrl = serviceUrl;
17
+ this.manifest = initialManifest;
18
+ }
19
+ /**
20
+ * Start background token refresh
21
+ * Checks every 5 minutes and refreshes tokens expiring in <10 minutes
22
+ */
23
+ startBackgroundRefresh() {
24
+ if (this.refreshInterval)
25
+ return;
26
+ // Check every 5 minutes
27
+ const checkInterval = 5 * 60 * 1000;
28
+ this.refreshInterval = setInterval(async () => {
29
+ await this.refreshIfNeeded();
30
+ }, checkInterval);
31
+ console.log('🔄 Token refresh background task started');
32
+ }
33
+ /**
34
+ * Stop background token refresh
35
+ */
36
+ stopBackgroundRefresh() {
37
+ if (this.refreshInterval) {
38
+ clearInterval(this.refreshInterval);
39
+ this.refreshInterval = null;
40
+ console.log('⏸️ Token refresh background task stopped');
41
+ }
42
+ }
43
+ /**
44
+ * Check if any tokens need refresh and refresh if needed
45
+ */
46
+ async refreshIfNeeded() {
47
+ if (this.isRefreshing || !this.manifest)
48
+ return;
49
+ const now = Date.now();
50
+ const tenMinutes = 10 * 60 * 1000;
51
+ let needsRefresh = false;
52
+ // Check if any tokens expire in the next 10 minutes
53
+ for (const [category, token] of Object.entries(this.manifest.tokens)) {
54
+ if (token && token.expiresAt) {
55
+ const timeUntilExpiry = token.expiresAt - now;
56
+ if (timeUntilExpiry < tenMinutes) {
57
+ console.log(`⚠️ ${category} token expires in ${Math.round(timeUntilExpiry / 1000 / 60)} minutes`);
58
+ needsRefresh = true;
59
+ break;
60
+ }
61
+ }
62
+ }
63
+ if (needsRefresh) {
64
+ await this.refreshTokens();
65
+ }
66
+ }
67
+ /**
68
+ * Force refresh all tokens from service
69
+ */
70
+ async refreshTokens() {
71
+ if (this.isRefreshing) {
72
+ console.log('⏳ Token refresh already in progress...');
73
+ return false;
74
+ }
75
+ this.isRefreshing = true;
76
+ try {
77
+ console.log('🔄 Refreshing tokens from service...');
78
+ const newManifest = await fetchManifest(this.botToken, this.serviceUrl);
79
+ if (!newManifest) {
80
+ console.error('❌ Failed to refresh tokens - service unavailable');
81
+ this.isRefreshing = false;
82
+ return false;
83
+ }
84
+ this.manifest = newManifest;
85
+ console.log('✅ Tokens refreshed successfully');
86
+ this.isRefreshing = false;
87
+ return true;
88
+ }
89
+ catch (error) {
90
+ console.error('❌ Token refresh failed:', error);
91
+ this.isRefreshing = false;
92
+ return false;
93
+ }
94
+ }
95
+ /**
96
+ * Get a valid token for a category
97
+ * Automatically refreshes if token is expired or missing
98
+ */
99
+ async getToken(category) {
100
+ if (!this.manifest) {
101
+ console.error(`❌ No manifest available for ${category}`);
102
+ return null;
103
+ }
104
+ const token = this.manifest.tokens[category];
105
+ if (!token) {
106
+ console.error(`❌ No ${category} token in manifest`);
107
+ return null;
108
+ }
109
+ // Check if token is expired or about to expire (within 2 minutes)
110
+ const now = Date.now();
111
+ const twoMinutes = 2 * 60 * 1000;
112
+ const isExpired = token.expiresAt <= now;
113
+ const isExpiringSoon = token.expiresAt - now < twoMinutes;
114
+ if (isExpired) {
115
+ console.log(`⚠️ ${category} token expired, refreshing...`);
116
+ const refreshed = await this.refreshTokens();
117
+ if (!refreshed) {
118
+ return null;
119
+ }
120
+ return this.manifest.tokens[category] || null;
121
+ }
122
+ if (isExpiringSoon) {
123
+ console.log(`⚠️ ${category} token expiring soon, refreshing...`);
124
+ // Refresh in background, but return current token
125
+ this.refreshTokens().catch(err => {
126
+ console.error('Background token refresh failed:', err);
127
+ });
128
+ }
129
+ return token;
130
+ }
131
+ /**
132
+ * Get the current manifest
133
+ */
134
+ getManifest() {
135
+ return this.manifest;
136
+ }
137
+ /**
138
+ * Update the manifest (useful for initial setup)
139
+ */
140
+ setManifest(manifest) {
141
+ this.manifest = manifest;
142
+ }
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,122 @@
1
+ import Database from 'better-sqlite3';
2
+ export class SessionDatabase {
3
+ db;
4
+ constructor(dbPath) {
5
+ this.db = new Database(dbPath);
6
+ }
7
+ /**
8
+ * Create a new session mapping
9
+ */
10
+ createMapping(mapping) {
11
+ const now = new Date().toISOString();
12
+ const stmt = this.db.prepare(`
13
+ INSERT INTO session_mappings (
14
+ discord_thread_id,
15
+ discord_channel_id,
16
+ discord_message_id,
17
+ session_id,
18
+ working_directory,
19
+ created_at,
20
+ last_active_at,
21
+ status
22
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
23
+ `);
24
+ stmt.run(mapping.discord_thread_id, mapping.discord_channel_id, mapping.discord_message_id, mapping.session_id, mapping.working_directory, now, now, mapping.status);
25
+ }
26
+ /**
27
+ * Get a session mapping by Discord thread ID
28
+ */
29
+ getMapping(threadId) {
30
+ const stmt = this.db.prepare(`
31
+ SELECT * FROM session_mappings WHERE discord_thread_id = ?
32
+ `);
33
+ return stmt.get(threadId);
34
+ }
35
+ /**
36
+ * Get session mapping by session ID
37
+ */
38
+ getMappingBySessionId(sessionId) {
39
+ const stmt = this.db.prepare(`
40
+ SELECT * FROM session_mappings WHERE session_id = ?
41
+ `);
42
+ return stmt.get(sessionId);
43
+ }
44
+ /**
45
+ * Get all sessions for a channel
46
+ */
47
+ getChannelSessions(channelId) {
48
+ const stmt = this.db.prepare(`
49
+ SELECT * FROM session_mappings WHERE discord_channel_id = ?
50
+ ORDER BY last_active_at DESC
51
+ `);
52
+ return stmt.all(channelId);
53
+ }
54
+ /**
55
+ * Get all active sessions
56
+ */
57
+ getAllActive() {
58
+ const stmt = this.db.prepare(`
59
+ SELECT * FROM session_mappings WHERE status = 'active'
60
+ ORDER BY last_active_at DESC
61
+ `);
62
+ return stmt.all();
63
+ }
64
+ /**
65
+ * Update last active timestamp for a session
66
+ */
67
+ updateLastActive(threadId) {
68
+ const stmt = this.db.prepare(`
69
+ UPDATE session_mappings
70
+ SET last_active_at = ?
71
+ WHERE discord_thread_id = ?
72
+ `);
73
+ stmt.run(new Date().toISOString(), threadId);
74
+ }
75
+ /**
76
+ * Update session ID for a thread
77
+ */
78
+ updateSessionId(threadId, newSessionId) {
79
+ const stmt = this.db.prepare(`
80
+ UPDATE session_mappings
81
+ SET session_id = ?, last_active_at = ?
82
+ WHERE discord_thread_id = ?
83
+ `);
84
+ stmt.run(newSessionId, new Date().toISOString(), threadId);
85
+ }
86
+ /**
87
+ * Archive a session
88
+ */
89
+ archiveSession(threadId) {
90
+ const stmt = this.db.prepare(`
91
+ UPDATE session_mappings
92
+ SET status = 'archived'
93
+ WHERE discord_thread_id = ?
94
+ `);
95
+ stmt.run(threadId);
96
+ }
97
+ /**
98
+ * Delete a session mapping
99
+ */
100
+ deleteMapping(threadId) {
101
+ const stmt = this.db.prepare(`
102
+ DELETE FROM session_mappings WHERE discord_thread_id = ?
103
+ `);
104
+ stmt.run(threadId);
105
+ }
106
+ /**
107
+ * Get count of active sessions
108
+ */
109
+ getActiveCount() {
110
+ const stmt = this.db.prepare(`
111
+ SELECT COUNT(*) as count FROM session_mappings WHERE status = 'active'
112
+ `);
113
+ const result = stmt.get();
114
+ return result.count;
115
+ }
116
+ /**
117
+ * Close the database connection
118
+ */
119
+ close() {
120
+ this.db.close();
121
+ }
122
+ }
@@ -0,0 +1,21 @@
1
+ import { createTool as createListJobs } from './cron/list_jobs.js';
2
+ import { createTool as createAddJob } from './cron/add_job.js';
3
+ import { createTool as createRemoveJob } from './cron/remove_job.js';
4
+ import { createTool as createUpdateJob } from './cron/update_job.js';
5
+ import { createTool as createShareFile } from './share_file.js';
6
+ /**
7
+ * Load built-in tools that don't require authentication
8
+ * These tools are always available regardless of manifest configuration
9
+ */
10
+ export function loadBuiltinTools(getCurrentWorkingDir, queueFileForSharing) {
11
+ const tools = [];
12
+ // Load cron management tools
13
+ tools.push(createListJobs(getCurrentWorkingDir));
14
+ tools.push(createAddJob(getCurrentWorkingDir));
15
+ tools.push(createRemoveJob(getCurrentWorkingDir));
16
+ tools.push(createUpdateJob(getCurrentWorkingDir));
17
+ // Load file sharing tool
18
+ tools.push(createShareFile(getCurrentWorkingDir, queueFileForSharing));
19
+ console.log(` ✓ Loaded ${tools.length} built-in tools`);
20
+ return tools;
21
+ }
@@ -0,0 +1,96 @@
1
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
2
+ import { z } from 'zod';
3
+ import { parseCronFile, validateCronSchedule } from '../../scheduler/parser.js';
4
+ import yaml from 'js-yaml';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ const schema = z.object({
8
+ name: z.string().describe('Unique name for this cron job (e.g., "Daily summary", "Weekly report")'),
9
+ schedule: z.string().describe('Cron schedule in 5-field format: "minute hour day month weekday". Examples: "0 9 * * *" (daily at 9am), "0 9 * * 1" (Mondays at 9am), "*/30 * * * *" (every 30 minutes)'),
10
+ task: z.string().describe('Description of the task for Claude to execute when this job runs (e.g., "Summarize recent changes", "Generate weekly report")'),
11
+ oneTime: z.boolean().optional().describe('Set to true for one-time tasks that should be removed after execution. Default: false')
12
+ });
13
+ export function createTool(getCwd) {
14
+ return tool('cron_add_job', 'Add a new scheduled cron job to this Discord channel. Use this instead of bash cron/at commands - jobs will execute autonomously and post results directly to the Discord channel. Always list jobs first to avoid duplicate names.', schema.shape, async (params) => {
15
+ try {
16
+ // Validate cron schedule format
17
+ if (!validateCronSchedule(params.schedule)) {
18
+ return {
19
+ content: [
20
+ {
21
+ type: 'text',
22
+ text: JSON.stringify({
23
+ error: `Invalid cron schedule format: "${params.schedule}". Must be 5 fields: minute hour day month weekday. Example: "0 9 * * *" for daily at 9am.`,
24
+ validFormat: 'minute hour day month weekday',
25
+ examples: [
26
+ '0 9 * * * - Every day at 9:00 AM',
27
+ '0 9 * * 1 - Every Monday at 9:00 AM',
28
+ '*/30 * * * * - Every 30 minutes',
29
+ '0 0 1 * * - First day of every month at midnight'
30
+ ]
31
+ }, null, 2)
32
+ }
33
+ ]
34
+ };
35
+ }
36
+ const cwd = getCwd();
37
+ const cronPath = path.join(cwd, '.claude-cron');
38
+ // Read existing jobs
39
+ const config = parseCronFile(cronPath);
40
+ // Check for duplicate name
41
+ if (config.jobs.some(job => job.name === params.name)) {
42
+ return {
43
+ content: [
44
+ {
45
+ type: 'text',
46
+ text: JSON.stringify({
47
+ error: `A job named "${params.name}" already exists. Use a different name or remove the existing job first.`,
48
+ existingJob: config.jobs.find(job => job.name === params.name)
49
+ }, null, 2)
50
+ }
51
+ ]
52
+ };
53
+ }
54
+ // Add new job
55
+ config.jobs.push({
56
+ name: params.name,
57
+ schedule: params.schedule,
58
+ task: params.task,
59
+ oneTime: params.oneTime || false
60
+ });
61
+ // Write back to file
62
+ const yamlContent = yaml.dump({ jobs: config.jobs });
63
+ fs.writeFileSync(cronPath, yamlContent, 'utf-8');
64
+ return {
65
+ content: [
66
+ {
67
+ type: 'text',
68
+ text: JSON.stringify({
69
+ success: true,
70
+ message: `Cron job "${params.name}" added successfully!`,
71
+ job: {
72
+ name: params.name,
73
+ schedule: params.schedule,
74
+ task: params.task,
75
+ oneTime: params.oneTime || false
76
+ },
77
+ note: 'The job will be automatically scheduled and will start running according to the schedule. Results will be posted to this Discord channel.'
78
+ }, null, 2)
79
+ }
80
+ ]
81
+ };
82
+ }
83
+ catch (error) {
84
+ return {
85
+ content: [
86
+ {
87
+ type: 'text',
88
+ text: JSON.stringify({
89
+ error: `Failed to add cron job: ${error instanceof Error ? error.message : 'Unknown error'}`
90
+ }, null, 2)
91
+ }
92
+ ]
93
+ };
94
+ }
95
+ });
96
+ }