Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
task-scheduler-manager.ts14.7 kB
/** * Task Scheduler Manager - Native Windows schtasks manipulation * Zero dependencies - uses direct schtasks.exe commands * Windows 10/11 only */ import { execSync } from 'child_process'; import { platform } from 'os'; import { logger } from '../../utils/logger.js'; import { homedir } from 'os'; import { join } from 'path'; import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'; export interface TaskEntry { id: string; cronExpression: string; command: string; comment?: string; } interface ScheduleInfo { scheduleType: 'MINUTE' | 'HOURLY' | 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'ONCE'; modifier?: string; // e.g., "5" for every 5 minutes, "MON,WED,FRI" for specific days day?: string; // For MONTHLY schedules - day of month (1-31) startTime?: string; // HH:mm format } export class TaskSchedulerManager { private static readonly TASK_PREFIX = 'NCP_'; private static readonly METADATA_DIR = join(homedir(), '.ncp', 'scheduler', 'windows-tasks'); constructor() { // Validate platform const currentPlatform = platform(); if (currentPlatform !== 'win32') { throw new Error('TaskSchedulerManager only supports Windows. Use CronManager for Linux or LaunchdManager for macOS.'); } // Ensure metadata directory exists if (!existsSync(TaskSchedulerManager.METADATA_DIR)) { mkdirSync(TaskSchedulerManager.METADATA_DIR, { recursive: true }); } logger.debug('[TaskSchedulerManager] Initialized for Windows'); } /** * Check if schtasks command is available */ private isSchTasksAvailable(): boolean { try { execSync('where schtasks', { stdio: 'pipe' }); return true; } catch { return false; } } /** * Get helpful error message when schtasks is not available */ private getSchTasksNotAvailableMessage(): string { return `Windows Task Scheduler (schtasks.exe) is not available. The Task Scheduler service may not be running. To fix this: 1. Open Services: services.msc 2. Find "Task Scheduler" → Right-click → Start Set Startup type to "Automatic" If schtasks.exe is still missing, run System File Checker: sfc /scannow Docs: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/schtasks (This message is visible to AI assistants for troubleshooting)`; } /** * Convert cron expression to Windows Task Scheduler schedule * This is a simplified conversion - some cron expressions cannot be represented in schtasks */ private cronToSchedule(cronExpression: string): ScheduleInfo { const parts = cronExpression.trim().split(/\s+/); if (parts.length !== 5) { throw new Error('Invalid cron expression - must have 5 fields'); } const [minute, hour, dayOfMonth, month, dayOfWeek] = parts; // MINUTE schedule - e.g., "*/5 * * * *" (every 5 minutes) if (minute.startsWith('*/') && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { const interval = minute.substring(2); return { scheduleType: 'MINUTE', modifier: interval }; } // HOURLY schedule - e.g., "0 * * * *" (every hour) or "30 */2 * * *" (every 2 hours at :30) if (hour.startsWith('*/') && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { const interval = hour.substring(2); const startMinute = minute === '*' ? '00' : minute.padStart(2, '0'); return { scheduleType: 'HOURLY', modifier: interval, startTime: `${startMinute}:00` // schtasks needs a start time for hourly }; } if (minute !== '*' && hour === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { return { scheduleType: 'HOURLY', modifier: '1', startTime: `${minute.padStart(2, '0')}:00` }; } // DAILY schedule - e.g., "0 9 * * *" (every day at 9:00) if (dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { const startHour = hour === '*' ? '00' : hour.padStart(2, '0'); const startMinute = minute === '*' ? '00' : minute.padStart(2, '0'); return { scheduleType: 'DAILY', modifier: '1', startTime: `${startHour}:${startMinute}` }; } // WEEKLY schedule - e.g., "0 9 * * 1" (every Monday at 9:00) if (dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') { const dayMap: Record<string, string> = { '0': 'SUN', '1': 'MON', '2': 'TUE', '3': 'WED', '4': 'THU', '5': 'FRI', '6': 'SAT', '7': 'SUN' }; let days: string; if (dayOfWeek.includes(',')) { // Multiple days: "1,3,5" -> "MON,WED,FRI" days = dayOfWeek.split(',').map(d => dayMap[d.trim()]).join(','); } else { days = dayMap[dayOfWeek]; } const startHour = hour === '*' ? '00' : hour.padStart(2, '0'); const startMinute = minute === '*' ? '00' : minute.padStart(2, '0'); return { scheduleType: 'WEEKLY', modifier: days, startTime: `${startHour}:${startMinute}` }; } // MONTHLY schedule - e.g., "0 9 15 * *" (15th of every month at 9:00) if (dayOfMonth !== '*' && month === '*' && dayOfWeek === '*') { const startHour = hour === '*' ? '00' : hour.padStart(2, '0'); const startMinute = minute === '*' ? '00' : minute.padStart(2, '0'); return { scheduleType: 'MONTHLY', modifier: '1', // Every 1 month day: dayOfMonth, // Day of month (1-31) startTime: `${startHour}:${startMinute}` }; } // If we can't convert, fall back to DAILY logger.warn(`[TaskSchedulerManager] Complex cron expression "${cronExpression}" converted to DAILY schedule`); const startHour = hour === '*' ? '00' : hour.padStart(2, '0'); const startMinute = minute === '*' ? '00' : minute.padStart(2, '0'); return { scheduleType: 'DAILY', modifier: '1', startTime: `${startHour}:${startMinute}` }; } /** * Get metadata file path for a job */ private getMetadataPath(jobId: string): string { return join(TaskSchedulerManager.METADATA_DIR, `${jobId}.json`); } /** * Save task metadata (cron expression and command) */ private saveMetadata(jobId: string, cronExpression: string, command: string): void { const metadata = { jobId, cronExpression, command }; const metadataPath = this.getMetadataPath(jobId); writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8'); } /** * Load task metadata */ private loadMetadata(jobId: string): { cronExpression: string; command: string } | null { const metadataPath = this.getMetadataPath(jobId); if (!existsSync(metadataPath)) { return null; } try { const data = readFileSync(metadataPath, 'utf-8'); return JSON.parse(data); } catch { return null; } } /** * Delete task metadata */ private deleteMetadata(jobId: string): void { const metadataPath = this.getMetadataPath(jobId); if (existsSync(metadataPath)) { try { const { unlinkSync } = require('fs'); unlinkSync(metadataPath); } catch (error) { logger.warn(`[TaskSchedulerManager] Failed to delete metadata for ${jobId}: ${error}`); } } } /** * Get task name with NCP prefix */ private getTaskName(jobId: string): string { return `${TaskSchedulerManager.TASK_PREFIX}${jobId}`; } /** * Add or update a scheduled task */ addJob(jobId: string, cronExpression: string, command: string): void { if (!this.isSchTasksAvailable()) { throw new Error(this.getSchTasksNotAvailableMessage()); } logger.info(`[TaskSchedulerManager] Adding job: ${jobId} with schedule: ${cronExpression}`); // Convert cron to schtasks schedule const schedule = this.cronToSchedule(cronExpression); const taskName = this.getTaskName(jobId); // Delete existing task if present try { execSync(`schtasks /Delete /TN "${taskName}" /F`, { stdio: 'pipe' }); logger.debug(`[TaskSchedulerManager] Deleted existing task: ${taskName}`); } catch { // Task doesn't exist, that's fine } // Build schtasks command let schTasksCmd = `schtasks /Create /TN "${taskName}" /TR "${command}" /SC ${schedule.scheduleType}`; if (schedule.modifier) { if (schedule.scheduleType === 'WEEKLY') { schTasksCmd += ` /D ${schedule.modifier}`; } else { schTasksCmd += ` /MO ${schedule.modifier}`; } } // Add day parameter for MONTHLY schedules if (schedule.day && schedule.scheduleType === 'MONTHLY') { schTasksCmd += ` /D ${schedule.day}`; } if (schedule.startTime) { schTasksCmd += ` /ST ${schedule.startTime}`; } // Run as current user, don't prompt for password schTasksCmd += ' /RU SYSTEM /F'; try { execSync(schTasksCmd, { stdio: 'pipe' }); logger.info(`[TaskSchedulerManager] Successfully created task ${taskName}`); // Save metadata for reconstruction this.saveMetadata(jobId, cronExpression, command); } catch (error) { throw new Error(`Failed to create Windows scheduled task: ${error instanceof Error ? error.message : String(error)}`); } } /** * Remove a scheduled task */ removeJob(jobId: string): void { logger.info(`[TaskSchedulerManager] Removing job: ${jobId}`); const taskName = this.getTaskName(jobId); try { execSync(`schtasks /Delete /TN "${taskName}" /F`, { stdio: 'pipe' }); logger.info(`[TaskSchedulerManager] Successfully deleted task ${taskName}`); } catch (error) { logger.warn(`[TaskSchedulerManager] Task ${taskName} not found or could not be deleted`); } // Delete metadata this.deleteMetadata(jobId); } /** * Get all NCP jobs from Task Scheduler */ getJobs(): TaskEntry[] { const jobs: TaskEntry[] = []; try { // List all tasks with NCP prefix const output = execSync('schtasks /Query /FO CSV /NH', { encoding: 'utf-8' }); const lines = output.split('\n').filter(line => line.trim()); for (const line of lines) { // CSV format: "TaskName","Next Run Time","Status" const match = line.match(/^"([^"]+)"/); if (match) { const taskName = match[1]; if (taskName.startsWith(`\\${TaskSchedulerManager.TASK_PREFIX}`)) { // Extract job ID const jobId = taskName.substring(TaskSchedulerManager.TASK_PREFIX.length + 1); // +1 for leading backslash // Load metadata const metadata = this.loadMetadata(jobId); if (metadata) { jobs.push({ id: jobId, cronExpression: metadata.cronExpression, command: metadata.command }); } } } } } catch (error) { logger.warn(`[TaskSchedulerManager] Failed to query tasks: ${error}`); } return jobs; } /** * Get a specific job */ getJob(jobId: string): TaskEntry | null { const taskName = this.getTaskName(jobId); try { // Query specific task execSync(`schtasks /Query /TN "${taskName}" /FO LIST`, { stdio: 'pipe' }); // Task exists, load metadata const metadata = this.loadMetadata(jobId); if (metadata) { return { id: jobId, cronExpression: metadata.cronExpression, command: metadata.command }; } } catch { // Task doesn't exist } return null; } /** * Check if a job exists in Task Scheduler */ hasJob(jobId: string): boolean { return this.getJob(jobId) !== null; } /** * Remove all NCP jobs from Task Scheduler */ removeAllJobs(): void { logger.info('[TaskSchedulerManager] Removing all NCP jobs from Task Scheduler'); const jobs = this.getJobs(); for (const job of jobs) { this.removeJob(job.id); } logger.info('[TaskSchedulerManager] Successfully removed all NCP jobs'); } /** * Validate cron expression format (re-uses same validation as CronManager) */ static validateCronExpression(expression: string): { valid: boolean; error?: string } { const parts = expression.trim().split(/\s+/); if (parts.length !== 5) { return { valid: false, error: `Cron expression must have exactly 5 fields (minute hour day month weekday), got ${parts.length}` }; } // Basic validation for each field const validators = [ { name: 'minute', range: [0, 59] }, { name: 'hour', range: [0, 23] }, { name: 'day', range: [1, 31] }, { name: 'month', range: [1, 12] }, { name: 'weekday', range: [0, 7] } // 0 and 7 both represent Sunday ]; for (let i = 0; i < parts.length; i++) { const part = parts[i]; const validator = validators[i]; // Allow wildcard if (part === '*') continue; // Validate */N pattern if (/^\*\/\d+$/.test(part)) { const step = parseInt(part.split('/')[1]); if (step < 1 || step > validator.range[1]) { return { valid: false, error: `Invalid ${validator.name} step value: ${step} (must be 1-${validator.range[1]})` }; } continue; } // Validate N-M range pattern if (/^\d+-\d+$/.test(part)) { const [start, end] = part.split('-').map(n => parseInt(n)); if (start < validator.range[0] || start > validator.range[1]) { return { valid: false, error: `Invalid ${validator.name} range start: ${start} (must be ${validator.range[0]}-${validator.range[1]})` }; } if (end < validator.range[0] || end > validator.range[1]) { return { valid: false, error: `Invalid ${validator.name} range end: ${end} (must be ${validator.range[0]}-${validator.range[1]})` }; } continue; } // Validate N,M,O list pattern if (/^[\d,]+$/.test(part)) { const values = part.split(',').map(n => parseInt(n)); for (const val of values) { if (isNaN(val) || val < validator.range[0] || val > validator.range[1]) { return { valid: false, error: `Invalid ${validator.name} value in list: ${val} (must be ${validator.range[0]}-${validator.range[1]})` }; } } continue; } // If we get here, it's an invalid pattern return { valid: false, error: `Invalid ${validator.name} pattern: ${part}` }; } return { valid: true }; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/portel-dev/ncp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server