UNPKG

@sirmrmarty/n8n-nodes-tmux-orchestrator

Version:

n8n nodes for orchestrating Claude AI agents through tmux sessions

629 lines 25.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TmuxScheduler = void 0; const n8n_workflow_1 = require("n8n-workflow"); const child_process_1 = require("child_process"); const tmuxBridge_1 = require("../../utils/tmuxBridge"); const paths_1 = require("../../utils/paths"); const fs_1 = require("fs"); class TmuxScheduler { constructor() { this.description = { displayName: 'Tmux Scheduler', name: 'tmuxScheduler', group: ['transform'], version: 1, subtitle: '={{$parameter["operation"]}}', description: 'Schedule agent check-ins and automated tasks', defaults: { name: 'Tmux Scheduler', }, inputs: ["main"], outputs: ["main"], credentials: [ { name: 'tmuxOrchestratorApi', required: false, }, ], properties: [ { displayName: 'Operation', name: 'operation', type: 'options', noDataExpression: true, options: [ { name: 'Schedule Check-In', value: 'scheduleCheckIn', description: 'Schedule a future agent check-in', }, { name: 'Create Reminder', value: 'createReminder', description: 'Set a reminder for an agent', }, { name: 'Manage Cron Jobs', value: 'manageCronJobs', description: 'Create or manage recurring tasks', }, { name: 'List Scheduled Tasks', value: 'listScheduledTasks', description: 'View all scheduled tasks', }, { name: 'Cancel Task', value: 'cancelTask', description: 'Cancel a scheduled task', }, { name: 'Batch Schedule', value: 'batchSchedule', description: 'Schedule multiple tasks at once', }, ], default: 'scheduleCheckIn', }, { displayName: 'Target Window', name: 'targetWindow', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['scheduleCheckIn', 'createReminder'], }, }, description: 'Target window (e.g., session:window)', }, { displayName: 'Minutes Until Check-In', name: 'minutesUntil', type: 'number', default: 30, displayOptions: { show: { operation: ['scheduleCheckIn'], }, }, description: 'Minutes until the check-in occurs', }, { displayName: 'Check-In Note', name: 'checkInNote', type: 'string', typeOptions: { rows: 3, }, default: '', displayOptions: { show: { operation: ['scheduleCheckIn'], }, }, description: 'Note or instructions for the check-in', }, { displayName: 'Reminder Message', name: 'reminderMessage', type: 'string', typeOptions: { rows: 3, }, default: '', required: true, displayOptions: { show: { operation: ['createReminder'], }, }, description: 'Reminder message to send', }, { displayName: 'Reminder Time', name: 'reminderTime', type: 'dateTime', default: '', displayOptions: { show: { operation: ['createReminder'], }, }, description: 'When to send the reminder', }, { displayName: 'Cron Action', name: 'cronAction', type: 'options', options: [ { name: 'Create', value: 'create', }, { name: 'List', value: 'list', }, { name: 'Delete', value: 'delete', }, ], default: 'create', displayOptions: { show: { operation: ['manageCronJobs'], }, }, description: 'Cron job action to perform', }, { displayName: 'Cron Schedule', name: 'cronSchedule', type: 'string', default: '0 */1 * * *', displayOptions: { show: { operation: ['manageCronJobs'], cronAction: ['create'], }, }, description: 'Cron schedule expression (e.g., "0 */1 * * *" for hourly)', }, { displayName: 'Cron Command', name: 'cronCommand', type: 'string', typeOptions: { rows: 3, }, default: '', displayOptions: { show: { operation: ['manageCronJobs'], cronAction: ['create'], }, }, description: 'Command to execute on schedule', }, { displayName: 'Cron Job ID', name: 'cronJobId', type: 'string', default: '', displayOptions: { show: { operation: ['manageCronJobs'], cronAction: ['delete'], }, }, description: 'ID of cron job to delete', }, { displayName: 'Task ID', name: 'taskId', type: 'string', default: '', required: true, displayOptions: { show: { operation: ['cancelTask'], }, }, description: 'ID of the task to cancel', }, { displayName: 'Schedule Tasks', name: 'scheduleTasks', type: 'json', default: '[\n {\n "targetWindow": "session:0",\n "minutes": 30,\n "note": "Check progress"\n }\n]', displayOptions: { show: { operation: ['batchSchedule'], }, }, description: 'JSON array of tasks to schedule', }, ], }; } async execute() { const items = this.getInputData(); const returnData = []; const operation = this.getNodeParameter('operation', 0); let bridgeConfig = {}; try { const credentials = await this.getCredentials('tmuxOrchestratorApi'); if (credentials?.useExternalScripts && credentials?.scriptsDirectory) { bridgeConfig.externalScriptsDir = credentials.scriptsDirectory; } if (credentials?.projectBasePath) { bridgeConfig.projectBasePath = credentials.projectBasePath; } } catch { } const bridge = new tmuxBridge_1.TmuxBridge(bridgeConfig); for (let i = 0; i < items.length; i++) { try { let result = {}; switch (operation) { case 'scheduleCheckIn': result = await TmuxScheduler.prototype.scheduleCheckIn(this, i, bridge); break; case 'createReminder': result = await TmuxScheduler.prototype.createReminder(this, i, bridge); break; case 'manageCronJobs': result = await TmuxScheduler.prototype.manageCronJobs(this, i); break; case 'listScheduledTasks': result = await TmuxScheduler.prototype.listScheduledTasks(); break; case 'cancelTask': result = await TmuxScheduler.prototype.cancelTask(this, i); break; case 'batchSchedule': result = await TmuxScheduler.prototype.batchSchedule(this, i, bridge); break; } returnData.push({ json: result, pairedItem: i, }); } catch (error) { if (this.continueOnFail()) { returnData.push({ json: { error: error.message }, pairedItem: i, }); } else { throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex: i, }); } } } return [returnData]; } async scheduleCheckIn(context, itemIndex, bridge) { const targetWindow = context.getNodeParameter('targetWindow', itemIndex); const minutesUntil = context.getNodeParameter('minutesUntil', itemIndex); const checkInNote = context.getNodeParameter('checkInNote', itemIndex, ''); try { const success = await bridge.scheduleCheckIn(minutesUntil, checkInNote, targetWindow); if (!success) { const scheduledTime = new Date(Date.now() + minutesUntil * 60000); const atTime = scheduledTime.toTimeString().slice(0, 5); try { const pathResolver = new paths_1.PathResolver(); let externalDir; try { const credentials = await context.getCredentials('tmuxOrchestratorApi'); if (credentials?.useExternalScripts && credentials?.scriptsDirectory) { externalDir = credentials.scriptsDirectory; } } catch { } const scriptPath = pathResolver.getScriptPath('send-claude-message.sh', externalDir); const command = `echo "${scriptPath} ${targetWindow} 'SCHEDULED CHECK-IN: ${checkInNote}'" | at ${atTime}`; (0, child_process_1.execSync)(command); } catch { throw new Error('Unable to schedule check-in: send-claude-message.sh script not found'); } } return { success: true, targetWindow, minutesUntil, scheduledTime: new Date(Date.now() + minutesUntil * 60000).toISOString(), note: checkInNote, message: `Check-in scheduled for ${targetWindow} in ${minutesUntil} minutes`, }; } catch (error) { throw new Error(`Failed to schedule check-in: ${error.message}`); } } async createReminder(context, itemIndex, bridge) { const targetWindow = context.getNodeParameter('targetWindow', itemIndex); const reminderMessage = context.getNodeParameter('reminderMessage', itemIndex); const reminderTime = context.getNodeParameter('reminderTime', itemIndex, ''); try { let scheduledTime; let minutesUntil; if (reminderTime) { scheduledTime = new Date(reminderTime); minutesUntil = Math.floor((scheduledTime.getTime() - Date.now()) / 60000); } else { minutesUntil = 30; scheduledTime = new Date(Date.now() + minutesUntil * 60000); } if (minutesUntil <= 0) { throw new Error('Reminder time must be in the future'); } const reminderNote = `REMINDER: ${reminderMessage}`; const success = await bridge.scheduleCheckIn(minutesUntil, reminderNote, targetWindow); const remindersFile = '/tmp/tmux-reminders.json'; let reminders = []; try { await fs_1.promises.access(remindersFile); const content = await fs_1.promises.readFile(remindersFile, 'utf8'); reminders = JSON.parse(content); } catch { } const reminderId = `REM-${Date.now()}`; reminders.push({ id: reminderId, targetWindow, message: reminderMessage, scheduledTime: scheduledTime.toISOString(), createdAt: new Date().toISOString(), }); await fs_1.promises.writeFile(remindersFile, JSON.stringify(reminders, null, 2)); return { success: true, reminderId, targetWindow, message: reminderMessage, scheduledTime: scheduledTime.toISOString(), minutesUntil, }; } catch (error) { throw new Error(`Failed to create reminder: ${error.message}`); } } async manageCronJobs(context, itemIndex) { const cronAction = context.getNodeParameter('cronAction', itemIndex); try { switch (cronAction) { case 'create': return await TmuxScheduler.prototype.createCronJob(context, itemIndex); case 'list': return await TmuxScheduler.prototype.listCronJobs(); case 'delete': return await TmuxScheduler.prototype.deleteCronJob(context, itemIndex); default: throw new Error(`Unknown cron action: ${cronAction}`); } } catch (error) { throw new Error(`Failed to manage cron jobs: ${error.message}`); } } async createCronJob(context, itemIndex) { const cronSchedule = context.getNodeParameter('cronSchedule', itemIndex); const cronCommand = context.getNodeParameter('cronCommand', itemIndex); try { const cronId = `TMUX-CRON-${Date.now()}`; const cronEntry = `${cronSchedule} ${cronCommand} # ${cronId}`; const currentCrontab = (0, child_process_1.execSync)('crontab -l 2>/dev/null || true').toString(); const newCrontab = currentCrontab + '\n' + cronEntry; const tempFile = `/tmp/crontab-${Date.now()}`; await fs_1.promises.writeFile(tempFile, newCrontab); (0, child_process_1.execSync)(`crontab ${tempFile}`); await fs_1.promises.unlink(tempFile); return { success: true, cronId, schedule: cronSchedule, command: cronCommand, message: 'Cron job created successfully', }; } catch (error) { throw new Error(`Failed to create cron job: ${error.message}`); } } async listCronJobs() { try { const crontab = (0, child_process_1.execSync)('crontab -l 2>/dev/null || true').toString(); const lines = crontab.split('\n').filter((line) => line.trim() && !line.startsWith('#')); const cronJobs = lines.map((line) => { const match = line.match(/^([\S\s]+?)\s+#\s*(TMUX-CRON-\d+)$/); if (match) { const parts = match[1].split(/\s+/); const schedule = parts.slice(0, 5).join(' '); const command = parts.slice(5).join(' '); return { id: match[2], schedule, command, raw: line, }; } return { schedule: 'Unknown', command: line, raw: line, }; }); return { success: true, count: cronJobs.length, cronJobs, }; } catch (error) { throw new Error(`Failed to list cron jobs: ${error.message}`); } } async deleteCronJob(context, itemIndex) { const cronJobId = context.getNodeParameter('cronJobId', itemIndex); try { const currentCrontab = (0, child_process_1.execSync)('crontab -l 2>/dev/null || true').toString(); const lines = currentCrontab.split('\n'); const filteredLines = lines.filter((line) => !line.includes(cronJobId)); if (lines.length === filteredLines.length) { throw new Error(`Cron job ${cronJobId} not found`); } const tempFile = `/tmp/crontab-${Date.now()}`; await fs_1.promises.writeFile(tempFile, filteredLines.join('\n')); (0, child_process_1.execSync)(`crontab ${tempFile}`); await fs_1.promises.unlink(tempFile); return { success: true, deletedId: cronJobId, message: 'Cron job deleted successfully', }; } catch (error) { throw new Error(`Failed to delete cron job: ${error.message}`); } } async listScheduledTasks() { try { const tasks = { atJobs: [], cronJobs: [], reminders: [], }; try { const atOutput = (0, child_process_1.execSync)('atq').toString(); const lines = atOutput.trim().split('\n'); tasks.atJobs = lines.map((line) => { const parts = line.split(/\s+/); return { jobId: parts[0], scheduledTime: parts.slice(1, 6).join(' '), queue: parts[6], user: parts[7], }; }); } catch (e) { } try { const cronResult = await this.listCronJobs(); tasks.cronJobs = cronResult.cronJobs; } catch (e) { } const remindersFile = '/tmp/tmux-reminders.json'; try { await fs_1.promises.access(remindersFile); const content = await fs_1.promises.readFile(remindersFile, 'utf8'); tasks.reminders = JSON.parse(content); } catch { } return { success: true, tasks, totalScheduled: tasks.atJobs.length + tasks.cronJobs.length + tasks.reminders.length, timestamp: new Date().toISOString(), }; } catch (error) { throw new Error(`Failed to list scheduled tasks: ${error.message}`); } } async cancelTask(context, itemIndex) { const taskId = context.getNodeParameter('taskId', itemIndex); try { let cancelled = false; let taskType = ''; try { (0, child_process_1.execSync)(`atrm ${taskId}`); cancelled = true; taskType = 'at job'; } catch (e) { } if (!cancelled) { const remindersFile = '/tmp/tmux-reminders.json'; try { await fs_1.promises.access(remindersFile); const content = await fs_1.promises.readFile(remindersFile, 'utf8'); let reminders = JSON.parse(content); const originalLength = reminders.length; reminders = reminders.filter(r => r.id !== taskId); if (reminders.length < originalLength) { await fs_1.promises.writeFile(remindersFile, JSON.stringify(reminders, null, 2)); cancelled = true; taskType = 'reminder'; } } catch { } } if (!cancelled && taskId.startsWith('TMUX-CRON-')) { await TmuxScheduler.prototype.deleteCronJob(context, itemIndex); cancelled = true; taskType = 'cron job'; } if (!cancelled) { throw new Error(`Task ${taskId} not found`); } return { success: true, cancelledTaskId: taskId, taskType, message: `${taskType} cancelled successfully`, }; } catch (error) { throw new Error(`Failed to cancel task: ${error.message}`); } } async batchSchedule(context, itemIndex, bridge) { const scheduleTasks = context.getNodeParameter('scheduleTasks', itemIndex); try { const tasks = JSON.parse(scheduleTasks); if (!Array.isArray(tasks)) { throw new Error('Schedule tasks must be an array'); } const results = []; for (const task of tasks) { if (!task.targetWindow || !task.minutes) { results.push({ error: 'Missing required fields: targetWindow and minutes', task, }); continue; } try { const success = await bridge.scheduleCheckIn(task.minutes, task.note || 'Batch scheduled check-in', task.targetWindow); results.push({ success: true, targetWindow: task.targetWindow, minutes: task.minutes, note: task.note, scheduledTime: new Date(Date.now() + task.minutes * 60000).toISOString(), }); } catch (error) { results.push({ error: error.message, task, }); } } const successCount = results.filter((r) => r.success).length; const failureCount = results.filter((r) => r.error).length; return { success: true, totalTasks: tasks.length, successCount, failureCount, results, timestamp: new Date().toISOString(), }; } catch (error) { throw new Error(`Failed to batch schedule: ${error.message}`); } } } exports.TmuxScheduler = TmuxScheduler; //# sourceMappingURL=TmuxScheduler.node.js.map