Add built-in scheduler with group-scoped tasks

- Custom nanoclaw MCP server with scheduling tools (schedule_task,
  list_tasks, get_task, update_task, pause/resume/cancel_task, send_message)
- Tasks run as full agents in their group's context
- Support for cron, interval, and one-time schedules
- Task run logging with duration and results
- Main channel has Bash access for admin tasks (query DB, manage groups)
- Other groups restricted to file operations only
- Updated docs and requirements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-01-31 21:00:37 +02:00
parent 423d45c52e
commit fa13b14dae
12 changed files with 772 additions and 55 deletions

117
src/db.ts
View File

@@ -2,7 +2,7 @@ import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { proto } from '@whiskeysockets/baileys';
import { NewMessage } from './types.js';
import { NewMessage, ScheduledTask, TaskRunLog } from './types.js';
import { STORE_DIR } from './config.js';
let db: Database.Database;
@@ -30,6 +30,34 @@ export function initDatabase(): void {
FOREIGN KEY (chat_jid) REFERENCES chats(jid)
);
CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp);
CREATE TABLE IF NOT EXISTS scheduled_tasks (
id TEXT PRIMARY KEY,
group_folder TEXT NOT NULL,
chat_jid TEXT NOT NULL,
prompt TEXT NOT NULL,
schedule_type TEXT NOT NULL,
schedule_value TEXT NOT NULL,
next_run TEXT,
last_run TEXT,
last_result TEXT,
status TEXT DEFAULT 'active',
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_next_run ON scheduled_tasks(next_run);
CREATE INDEX IF NOT EXISTS idx_status ON scheduled_tasks(status);
CREATE TABLE IF NOT EXISTS task_run_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task_id TEXT NOT NULL,
run_at TEXT NOT NULL,
duration_ms INTEGER NOT NULL,
status TEXT NOT NULL,
result TEXT,
error TEXT,
FOREIGN KEY (task_id) REFERENCES scheduled_tasks(id)
);
CREATE INDEX IF NOT EXISTS idx_task_run_logs ON task_run_logs(task_id, run_at);
`);
// Add sender_name column if it doesn't exist (migration for existing DBs)
@@ -89,3 +117,90 @@ export function getMessagesSince(chatJid: string, sinceTimestamp: string): NewMe
`;
return db.prepare(sql).all(chatJid, sinceTimestamp) as NewMessage[];
}
// Scheduled Tasks
export function createTask(task: Omit<ScheduledTask, 'last_run' | 'last_result'>): void {
db.prepare(`
INSERT INTO scheduled_tasks (id, group_folder, chat_jid, prompt, schedule_type, schedule_value, next_run, status, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
task.id,
task.group_folder,
task.chat_jid,
task.prompt,
task.schedule_type,
task.schedule_value,
task.next_run,
task.status,
task.created_at
);
}
export function getTaskById(id: string): ScheduledTask | undefined {
return db.prepare('SELECT * FROM scheduled_tasks WHERE id = ?').get(id) as ScheduledTask | undefined;
}
export function getTasksForGroup(groupFolder: string): ScheduledTask[] {
return db.prepare('SELECT * FROM scheduled_tasks WHERE group_folder = ? ORDER BY created_at DESC').all(groupFolder) as ScheduledTask[];
}
export function getAllTasks(): ScheduledTask[] {
return db.prepare('SELECT * FROM scheduled_tasks ORDER BY created_at DESC').all() as ScheduledTask[];
}
export function updateTask(id: string, updates: Partial<Pick<ScheduledTask, 'prompt' | 'schedule_type' | 'schedule_value' | 'next_run' | 'status'>>): void {
const fields: string[] = [];
const values: unknown[] = [];
if (updates.prompt !== undefined) { fields.push('prompt = ?'); values.push(updates.prompt); }
if (updates.schedule_type !== undefined) { fields.push('schedule_type = ?'); values.push(updates.schedule_type); }
if (updates.schedule_value !== undefined) { fields.push('schedule_value = ?'); values.push(updates.schedule_value); }
if (updates.next_run !== undefined) { fields.push('next_run = ?'); values.push(updates.next_run); }
if (updates.status !== undefined) { fields.push('status = ?'); values.push(updates.status); }
if (fields.length === 0) return;
values.push(id);
db.prepare(`UPDATE scheduled_tasks SET ${fields.join(', ')} WHERE id = ?`).run(...values);
}
export function deleteTask(id: string): void {
db.prepare('DELETE FROM scheduled_tasks WHERE id = ?').run(id);
db.prepare('DELETE FROM task_run_logs WHERE task_id = ?').run(id);
}
export function getDueTasks(): ScheduledTask[] {
const now = new Date().toISOString();
return db.prepare(`
SELECT * FROM scheduled_tasks
WHERE status = 'active' AND next_run IS NOT NULL AND next_run <= ?
ORDER BY next_run
`).all(now) as ScheduledTask[];
}
export function updateTaskAfterRun(id: string, nextRun: string | null, lastResult: string): void {
const now = new Date().toISOString();
db.prepare(`
UPDATE scheduled_tasks
SET next_run = ?, last_run = ?, last_result = ?, status = CASE WHEN ? IS NULL THEN 'completed' ELSE status END
WHERE id = ?
`).run(nextRun, now, lastResult, nextRun, id);
}
export function logTaskRun(log: TaskRunLog): void {
db.prepare(`
INSERT INTO task_run_logs (task_id, run_at, duration_ms, status, result, error)
VALUES (?, ?, ?, ?, ?, ?)
`).run(log.task_id, log.run_at, log.duration_ms, log.status, log.result, log.error);
}
export function getTaskRunLogs(taskId: string, limit = 10): TaskRunLog[] {
return db.prepare(`
SELECT task_id, run_at, duration_ms, status, result, error
FROM task_run_logs
WHERE task_id = ?
ORDER BY run_at DESC
LIMIT ?
`).all(taskId, limit) as TaskRunLog[];
}