openclaw/secure/scheduler.ts
Claude a44d683dd7
feat: wire up sandbox/scheduler commands + complete AssureBot rebrand
- Add /sandbox, /schedule, /tasks, /deltask commands to telegram bot
- Wire sandbox and scheduler dependencies through to telegram handler
- Complete AssureBot rebrand across all files (audit, config, webhooks, etc.)
- Update secure/README.md with correct branding
- Update X-AssureBot-Token header for webhooks
- Update ASSUREBOT_GATEWAY_TOKEN env var

https://claude.ai/code/session_015VqJ7gN4vaxtYfYc92UjLs
2026-01-30 07:04:49 +00:00

271 lines
6.8 KiB
TypeScript

/**
* AssureBot - Task Scheduler
*
* Simple cron-like scheduler for recurring tasks.
* Stores jobs in memory or optionally persists to file.
*/
import { CronJob } from "cron";
import type { SecureConfig } from "./config.js";
import type { AuditLogger } from "./audit.js";
import type { AgentCore } from "./agent.js";
import type { Bot } from "grammy";
import { sendToUser } from "./telegram.js";
export type ScheduledTask = {
id: string;
name: string;
schedule: string; // Cron expression
prompt: string; // What to ask the AI
enabled: boolean;
lastRun?: Date;
lastStatus?: "ok" | "error";
lastError?: string;
};
export type Scheduler = {
addTask: (task: Omit<ScheduledTask, "id">) => string;
removeTask: (id: string) => boolean;
enableTask: (id: string, enabled: boolean) => boolean;
listTasks: () => ScheduledTask[];
runTask: (id: string) => Promise<void>;
start: () => void;
stop: () => void;
};
export type SchedulerDeps = {
config: SecureConfig;
audit: AuditLogger;
agent: AgentCore;
telegramBot: Bot;
};
function generateId(): string {
return Math.random().toString(36).substring(2, 10);
}
export function createScheduler(deps: SchedulerDeps): Scheduler {
const { config, audit, agent, telegramBot } = deps;
const tasks = new Map<string, ScheduledTask>();
const cronJobs = new Map<string, CronJob<null, unknown>>();
async function executeTask(task: ScheduledTask): Promise<void> {
const startTime = Date.now();
try {
// Run the AI with the task prompt
const response = await agent.chat([
{ role: "user", content: task.prompt },
]);
// Notify users
const message = `**Scheduled Task: ${task.name}**\n\n${response.text}`;
for (const userId of config.telegram.allowedUsers) {
await sendToUser(telegramBot, userId, message);
}
task.lastRun = new Date();
task.lastStatus = "ok";
task.lastError = undefined;
audit.cron({
jobId: task.id,
jobName: task.name,
status: "ok",
durationMs: Date.now() - startTime,
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
task.lastRun = new Date();
task.lastStatus = "error";
task.lastError = errorMsg;
audit.cron({
jobId: task.id,
jobName: task.name,
status: "error",
error: errorMsg,
durationMs: Date.now() - startTime,
});
// Notify about error
const message = `**Scheduled Task Failed: ${task.name}**\n\nError: ${errorMsg}`;
for (const userId of config.telegram.allowedUsers) {
await sendToUser(telegramBot, userId, message);
}
}
}
function scheduleTask(task: ScheduledTask): void {
// Remove existing job if any
const existing = cronJobs.get(task.id);
if (existing) {
existing.stop();
cronJobs.delete(task.id);
}
if (!task.enabled || !config.scheduler.enabled) {
return;
}
try {
const job = new CronJob(
task.schedule,
() => {
void executeTask(task);
},
null,
true, // Start immediately
undefined, // Default timezone
undefined,
false // Don't run on init
);
cronJobs.set(task.id, job);
} catch (err) {
console.error(`[scheduler] Failed to schedule task ${task.id}:`, err);
}
}
return {
addTask(taskInput: Omit<ScheduledTask, "id">): string {
const id = generateId();
const task: ScheduledTask = { ...taskInput, id };
tasks.set(id, task);
scheduleTask(task);
return id;
},
removeTask(id: string): boolean {
const task = tasks.get(id);
if (!task) return false;
const job = cronJobs.get(id);
if (job) {
job.stop();
cronJobs.delete(id);
}
tasks.delete(id);
return true;
},
enableTask(id: string, enabled: boolean): boolean {
const task = tasks.get(id);
if (!task) return false;
task.enabled = enabled;
scheduleTask(task);
return true;
},
listTasks(): ScheduledTask[] {
return Array.from(tasks.values());
},
async runTask(id: string): Promise<void> {
const task = tasks.get(id);
if (!task) {
throw new Error(`Task not found: ${id}`);
}
await executeTask(task);
},
start(): void {
if (!config.scheduler.enabled) {
console.log("[scheduler] Scheduler is disabled");
return;
}
console.log("[scheduler] Starting scheduler...");
for (const task of tasks.values()) {
scheduleTask(task);
}
},
stop(): void {
console.log("[scheduler] Stopping scheduler...");
for (const job of cronJobs.values()) {
job.stop();
}
cronJobs.clear();
},
};
}
/**
* Parse schedule from human-readable format
*/
export function parseSchedule(input: string): string | null {
const lower = input.toLowerCase().trim();
// Common patterns
const patterns: Record<string, string> = {
"every minute": "* * * * *",
"every 5 minutes": "*/5 * * * *",
"every 15 minutes": "*/15 * * * *",
"every 30 minutes": "*/30 * * * *",
"every hour": "0 * * * *",
hourly: "0 * * * *",
"every day": "0 9 * * *",
daily: "0 9 * * *",
"every morning": "0 9 * * *",
"every evening": "0 18 * * *",
"every week": "0 9 * * 1",
weekly: "0 9 * * 1",
"every monday": "0 9 * * 1",
"every tuesday": "0 9 * * 2",
"every wednesday": "0 9 * * 3",
"every thursday": "0 9 * * 4",
"every friday": "0 9 * * 5",
"every saturday": "0 9 * * 6",
"every sunday": "0 9 * * 0",
};
if (patterns[lower]) {
return patterns[lower];
}
// Check if it's already a valid cron expression (5 or 6 fields)
const parts = input.trim().split(/\s+/);
if (parts.length >= 5 && parts.length <= 6) {
return input.trim();
}
return null;
}
/**
* Format next run time
*/
export function formatNextRun(cronExpression: string): string {
try {
const job = new CronJob(cronExpression, () => {});
const nextDate = job.nextDate();
return nextDate.toLocaleString();
} catch {
return "Invalid schedule";
}
}
/**
* Built-in task templates
*/
export const taskTemplates = {
morningBriefing: {
name: "Morning Briefing",
schedule: "0 9 * * *", // 9 AM daily
prompt: "Give me a brief morning update. Include: current date, a motivational quote, and remind me to check my priorities for the day.",
},
weeklyReview: {
name: "Weekly Review",
schedule: "0 17 * * 5", // 5 PM on Fridays
prompt: "It's Friday. Help me reflect on the week. What should I consider for my weekly review?",
},
healthReminder: {
name: "Health Reminder",
schedule: "0 */2 * * *", // Every 2 hours
prompt: "Give me a brief health reminder (stretch, drink water, take a break). Keep it under 2 sentences.",
},
};