UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

136 lines 4.67 kB
import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { Cron } from 'croner'; import { parseCommandFile } from '../custom-commands/parser.js'; import { addScheduleRun, generateScheduleId, loadSchedules, saveSchedules, updateScheduleRun, } from './storage.js'; export class ScheduleRunner { cronJobs = new Map(); queue = []; isRunning = false; isProcessing = false; callbacks; constructor(callbacks) { this.callbacks = callbacks; } /** * Start all scheduled cron jobs */ async start() { if (this.isRunning) return; this.isRunning = true; const schedules = await loadSchedules(); const enabledSchedules = schedules.filter(s => s.enabled); for (const schedule of enabledSchedules) { this.registerCronJob(schedule); } } /** * Stop all cron jobs and clear the queue */ stop() { this.isRunning = false; for (const [, job] of this.cronJobs) { job.stop(); } this.cronJobs.clear(); this.queue = []; } /** * Get the number of active cron jobs */ getActiveJobCount() { return this.cronJobs.size; } /** * Get the current queue length */ getQueueLength() { return this.queue.length; } /** * Check if currently processing a job */ getIsProcessing() { return this.isProcessing; } registerCronJob(schedule) { const job = new Cron(schedule.cron, () => { this.enqueueJob(schedule); }); this.cronJobs.set(schedule.id, job); } enqueueJob(schedule) { // Don't duplicate — if this schedule is already queued, skip if (this.queue.some(s => s.id === schedule.id)) return; this.queue.push(schedule); void this.processQueue(); } async processQueue() { if (this.isProcessing || this.queue.length === 0) return; this.isProcessing = true; while (this.queue.length > 0 && this.isRunning) { const schedule = this.queue.shift(); if (schedule) { await this.executeJob(schedule); } } this.isProcessing = false; } async executeJob(schedule) { const run = { id: `run-${generateScheduleId()}`, scheduleId: schedule.id, command: schedule.command, startedAt: new Date().toISOString(), completedAt: null, status: 'running', }; await addScheduleRun(run); this.callbacks.onJobStart(schedule); try { // Clear messages for fresh context await this.callbacks.clearMessages(); // Load the schedule file from .nanocoder/schedules/ const filePath = join(process.cwd(), '.nanocoder', 'schedules', schedule.command); if (!existsSync(filePath)) { throw new Error(`Schedule file not found: ${schedule.command}. Ensure it exists in .nanocoder/schedules/`); } const parsed = parseCommandFile(filePath); const prompt = `[Executing scheduled command: ${schedule.command}]\n\n${parsed.content}`; await this.callbacks.handleMessageSubmit(prompt); // Wait for the conversation to complete await this.callbacks.waitForConversationComplete(); // Update run status run.completedAt = new Date().toISOString(); run.status = 'success'; await updateScheduleRun(run.id, { completedAt: run.completedAt, status: 'success', }); // Update lastRunAt on the schedule const schedules = await loadSchedules(); const idx = schedules.findIndex(s => s.id === schedule.id); if (idx !== -1 && schedules[idx]) { schedules[idx].lastRunAt = run.completedAt; await saveSchedules(schedules); } this.callbacks.onJobComplete(schedule, run); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); run.completedAt = new Date().toISOString(); run.status = 'error'; run.error = errorMsg; await updateScheduleRun(run.id, { completedAt: run.completedAt, status: 'error', error: errorMsg, }); this.callbacks.onJobError(schedule, errorMsg); } } } //# sourceMappingURL=runner.js.map