Spaces:
Runtime error
Runtime error
| /** | |
| * Hermes Cron/Task Scanner | |
| * | |
| * Read-only bridge that discovers Hermes Agent's scheduled cron jobs from: | |
| * - ~/.hermes/cron/jobs.json — Scheduled task definitions | |
| * - ~/.hermes/cron/output/{job_id}/ — Execution output files | |
| * | |
| * Follows the same throttled-scan pattern as claude-tasks.ts. | |
| */ | |
| import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs' | |
| import { join } from 'node:path' | |
| import { config } from './config' | |
| import { logger } from './logger' | |
| export interface HermesCronJob { | |
| id: string | |
| prompt: string | |
| schedule: string | |
| enabled: boolean | |
| lastRunAt: string | null | |
| lastOutput: string | null | |
| createdAt: string | null | |
| } | |
| export interface HermesTaskScanResult { | |
| cronJobs: HermesCronJob[] | |
| } | |
| function getHermesCronDir(): string { | |
| return join(config.homeDir, '.hermes', 'cron') | |
| } | |
| function peekLatestOutput(cronDir: string, jobId: string): { lastRunAt: string | null; lastOutput: string | null } { | |
| const outputDir = join(cronDir, 'output', jobId) | |
| try { | |
| if (!existsSync(outputDir) || !statSync(outputDir).isDirectory()) { | |
| return { lastRunAt: null, lastOutput: null } | |
| } | |
| const files = readdirSync(outputDir) | |
| .filter(f => f.endsWith('.md')) | |
| .sort() | |
| .reverse() | |
| if (files.length === 0) return { lastRunAt: null, lastOutput: null } | |
| const latestFile = files[0] | |
| // Filename is typically a timestamp like 2025-01-15T10-30-00.md | |
| const timestamp = latestFile.replace(/\.md$/, '').replace(/-/g, (m, i) => { | |
| // Convert filename back to ISO-ish timestamp | |
| return i > 9 ? ':' : m | |
| }) | |
| const filePath = join(outputDir, latestFile) | |
| let content: string | null = null | |
| try { | |
| const raw = readFileSync(filePath, 'utf-8') | |
| content = raw.slice(0, 500) | |
| } catch { /* ignore */ } | |
| return { | |
| lastRunAt: timestamp || null, | |
| lastOutput: content, | |
| } | |
| } catch { | |
| return { lastRunAt: null, lastOutput: null } | |
| } | |
| } | |
| function scanCronJobs(): HermesCronJob[] { | |
| const cronDir = getHermesCronDir() | |
| const jobsFile = join(cronDir, 'jobs.json') | |
| if (!existsSync(jobsFile)) return [] | |
| try { | |
| const raw = readFileSync(jobsFile, 'utf-8') | |
| const jobs = JSON.parse(raw) | |
| if (!Array.isArray(jobs)) return [] | |
| return jobs.map((job: any) => { | |
| const id = job.id || job.name || 'unknown' | |
| const { lastRunAt, lastOutput } = peekLatestOutput(cronDir, id) | |
| return { | |
| id, | |
| prompt: job.prompt || job.command || job.description || '', | |
| schedule: job.schedule || job.cron || job.interval || '', | |
| enabled: job.enabled !== false, | |
| lastRunAt: job.last_run_at || lastRunAt, | |
| lastOutput, | |
| createdAt: job.created_at || null, | |
| } | |
| }) | |
| } catch (err) { | |
| logger.warn({ err }, 'Failed to parse Hermes cron jobs') | |
| return [] | |
| } | |
| } | |
| // Throttle full disk scans | |
| let lastScanAt = 0 | |
| let cachedResult: HermesTaskScanResult = { cronJobs: [] } | |
| const SCAN_THROTTLE_MS = 30_000 | |
| export function getHermesTasks(force = false): HermesTaskScanResult { | |
| const now = Date.now() | |
| if (!force && lastScanAt > 0 && (now - lastScanAt) < SCAN_THROTTLE_MS) { | |
| return cachedResult | |
| } | |
| try { | |
| cachedResult = { cronJobs: scanCronJobs() } | |
| lastScanAt = now | |
| } catch (err) { | |
| logger.warn({ err }, 'Hermes task scan failed') | |
| } | |
| return cachedResult | |
| } | |