Skip to main content
Glama
portel-dev

NCP - Natural Context Provider

by portel-dev
cron-manager.ts11.7 kB
/** * Cron Manager - Native OS crontab manipulation * Zero dependencies - uses direct crontab binary commands * Unix/Linux/macOS only (Windows support via Task Scheduler would be separate) */ import { execSync } from 'child_process'; import { platform } from 'os'; import { logger } from '../../utils/logger.js'; export interface CronEntry { id: string; cronExpression: string; command: string; comment?: string; } export class CronManager { private static readonly MARKER_PREFIX = '# NCP_JOB:'; private static readonly NCP_SECTION_START = '# === NCP SCHEDULED JOBS - DO NOT EDIT MANUALLY ==='; private static readonly NCP_SECTION_END = '# === END NCP SCHEDULED JOBS ==='; constructor() { // Validate platform const currentPlatform = platform(); if (currentPlatform === 'win32') { throw new Error('CronManager does not support Windows. Use TaskSchedulerManager instead.'); } logger.debug('[CronManager] Initialized for platform: ' + currentPlatform); } /** * Check if crontab command is available */ private isCrontabAvailable(): boolean { try { execSync('which crontab', { stdio: 'pipe' }); return true; } catch { return false; } } /** * Get helpful error message when crontab is not available */ private getCrontabNotAvailableMessage(): string { // Detect distribution and provide specific command let installCmd = 'sudo apt-get update && sudo apt-get install cron'; // Default to Ubuntu/Debian try { const osRelease = execSync('cat /etc/os-release 2>/dev/null || echo ""', { encoding: 'utf-8' }); if (osRelease.includes('alpine')) { installCmd = 'apk add dcron && rc-update add dcron && rc-service dcron start'; } else if (osRelease.includes('fedora') || osRelease.includes('rhel') || osRelease.includes('centos') || osRelease.includes('rocky')) { installCmd = 'sudo dnf install cronie && sudo systemctl enable --now crond'; } else if (osRelease.includes('arch')) { installCmd = 'sudo pacman -S cronie && sudo systemctl enable --now cronie'; } } catch { // Use default if detection fails } return `crontab command not found. Cron needs to be installed. Install cron: ${installCmd} Verify installation: which crontab Docs: https://man7.org/linux/man-pages/man1/crontab.1.html (This message is visible to AI assistants for troubleshooting)`; } /** * Get current user's crontab */ private getCurrentCrontab(): string { if (!this.isCrontabAvailable()) { throw new Error(this.getCrontabNotAvailableMessage()); } try { return execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); } catch (error) { // If crontab returns error (e.g., "no crontab for user"), return empty string logger.debug('[CronManager] No existing crontab found, starting fresh'); return ''; } } /** * Write crontab content */ private writeCrontab(content: string): void { try { // Use heredoc to avoid shell escaping issues execSync('crontab -', { input: content, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); logger.debug('[CronManager] Successfully wrote crontab'); } catch (error) { throw new Error(`Failed to write crontab: ${error instanceof Error ? error.message : String(error)}`); } } /** * Parse NCP jobs from crontab */ private parseNCPJobs(crontab: string): CronEntry[] { const lines = crontab.split('\n'); const jobs: CronEntry[] = []; let inNCPSection = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line === CronManager.NCP_SECTION_START) { inNCPSection = true; continue; } if (line === CronManager.NCP_SECTION_END) { inNCPSection = false; continue; } if (inNCPSection && line.startsWith(CronManager.MARKER_PREFIX)) { // Extract job ID from comment const jobId = line.substring(CronManager.MARKER_PREFIX.length).trim(); // Next line should be the cron entry if (i + 1 < lines.length) { const cronLine = lines[i + 1]; const parsed = this.parseCronLine(cronLine); if (parsed) { jobs.push({ id: jobId, cronExpression: parsed.cronExpression, command: parsed.command }); } } } } return jobs; } /** * Parse a single cron line */ private parseCronLine(line: string): { cronExpression: string; command: string } | null { if (!line || line.startsWith('#') || !line.trim()) { return null; } // Cron format: minute hour day month weekday command // Split on whitespace, first 5 fields are schedule, rest is command const parts = line.trim().split(/\s+/); if (parts.length < 6) { return null; } const cronExpression = parts.slice(0, 5).join(' '); const command = parts.slice(5).join(' '); return { cronExpression, command }; } /** * Get non-NCP crontab entries */ private getNonNCPEntries(crontab: string): string { const lines = crontab.split('\n'); const nonNCPLines: string[] = []; let inNCPSection = false; for (const line of lines) { if (line === CronManager.NCP_SECTION_START) { inNCPSection = true; continue; } if (line === CronManager.NCP_SECTION_END) { inNCPSection = false; continue; } if (!inNCPSection) { nonNCPLines.push(line); } } // Remove trailing empty lines while (nonNCPLines.length > 0 && !nonNCPLines[nonNCPLines.length - 1].trim()) { nonNCPLines.pop(); } return nonNCPLines.join('\n'); } /** * Build NCP section content */ private buildNCPSection(jobs: CronEntry[]): string { if (jobs.length === 0) { return ''; } const lines = [CronManager.NCP_SECTION_START]; for (const job of jobs) { lines.push(`${CronManager.MARKER_PREFIX} ${job.id}`); lines.push(`${job.cronExpression} ${job.command}`); } lines.push(CronManager.NCP_SECTION_END); return lines.join('\n'); } /** * Add or update a cron job */ addJob(jobId: string, cronExpression: string, command: string): void { logger.info(`[CronManager] Adding job: ${jobId} with schedule: ${cronExpression}`); const currentCrontab = this.getCurrentCrontab(); const existingJobs = this.parseNCPJobs(currentCrontab); const nonNCPContent = this.getNonNCPEntries(currentCrontab); // Remove existing job with same ID if present const updatedJobs = existingJobs.filter(j => j.id !== jobId); // Add new job updatedJobs.push({ id: jobId, cronExpression, command }); // Build new crontab const ncpSection = this.buildNCPSection(updatedJobs); const newCrontab = nonNCPContent ? `${nonNCPContent}\n\n${ncpSection}\n` : `${ncpSection}\n`; this.writeCrontab(newCrontab); logger.info(`[CronManager] Successfully added job ${jobId} to crontab`); } /** * Remove a cron job */ removeJob(jobId: string): void { logger.info(`[CronManager] Removing job: ${jobId}`); const currentCrontab = this.getCurrentCrontab(); const existingJobs = this.parseNCPJobs(currentCrontab); const nonNCPContent = this.getNonNCPEntries(currentCrontab); // Remove job with matching ID const updatedJobs = existingJobs.filter(j => j.id !== jobId); if (updatedJobs.length === existingJobs.length) { logger.warn(`[CronManager] Job ${jobId} not found in crontab`); return; } // Build new crontab const ncpSection = this.buildNCPSection(updatedJobs); const newCrontab = ncpSection ? (nonNCPContent ? `${nonNCPContent}\n\n${ncpSection}\n` : `${ncpSection}\n`) : (nonNCPContent ? `${nonNCPContent}\n` : ''); this.writeCrontab(newCrontab); logger.info(`[CronManager] Successfully removed job ${jobId} from crontab`); } /** * Get all NCP jobs from crontab */ getJobs(): CronEntry[] { const currentCrontab = this.getCurrentCrontab(); return this.parseNCPJobs(currentCrontab); } /** * Get a specific job */ getJob(jobId: string): CronEntry | null { const jobs = this.getJobs(); return jobs.find(j => j.id === jobId) || null; } /** * Check if a job exists in crontab */ hasJob(jobId: string): boolean { return this.getJob(jobId) !== null; } /** * Remove all NCP jobs from crontab */ removeAllJobs(): void { logger.info('[CronManager] Removing all NCP jobs from crontab'); const currentCrontab = this.getCurrentCrontab(); const nonNCPContent = this.getNonNCPEntries(currentCrontab); const newCrontab = nonNCPContent ? `${nonNCPContent}\n` : ''; this.writeCrontab(newCrontab); logger.info('[CronManager] Successfully removed all NCP jobs'); } /** * Validate cron expression format */ 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