HuggingClaw-MissionControl / src /lib /hermes-tasks.ts
nyk
feat(refactor): ready for manual QA after main sync (#274)
b6ecafa unverified
Raw
History Blame Contribute Delete
3.35 kB
/**
* 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
}