import { Router, Request, Response } from "express"; import { loadSchedulerItems, saveSchedulerItems } from "../../utils/files/scheduler-io.js"; import { dispatchScheduler, type SchedulerActionInput } from "./schedulerHandlers.js"; import { respondWithDispatchResult, type DispatchSuccessResponse, type DispatchErrorResponse } from "./dispatchResponse.js"; import { API_ROUTES } from "../../../src/config/apiRoutes.js"; import { bindRoute } from "../../utils/router.js"; import { SESSION_ORIGINS } from "../../../src/types/session.js"; import { loadUserTasks, validateAndCreate, refreshUserTasks } from "../../workspace/skills/user-tasks.js"; import { saveUserTasks } from "../../utils/files/user-tasks-io.js"; import { startChat } from "./agent.js"; import { log } from "../../system/logger/index.js"; import { SCHEDULER_ACTIONS, TASK_ACTIONS } from "../../../src/plugins/scheduler/actions.js"; import { badRequest, notFound } from "../../utils/httpError.js"; import { asyncHandler } from "../../utils/asyncHandler.js"; import { makeUuid } from "../../utils/id.js"; const router = Router(); export interface ScheduledItem { id: string; title: string; createdAt: number; /** `unknown` values, not scalars: `sanitizeProps` only ever inspects * `endDate`, so any other JSON the LLM sends survives verbatim. Every * reader re-checks the value's type before using it. */ props: Record; } function loadItems(): ScheduledItem[] { return loadSchedulerItems([]); } function saveItems(items: ScheduledItem[]): void { saveSchedulerItems(items); } bindRoute(router, API_ROUTES.scheduler.list, (_req: Request, res: Response<{ data: { items: ScheduledItem[] } }>) => { res.json({ data: { items: loadItems() } }); }); interface SchedulerBody extends SchedulerActionInput { action: string; // Task-related fields name?: string; prompt?: string; schedule?: unknown; roleId?: string; } bindRoute( router, API_ROUTES.scheduler.dispatch, asyncHandler, Response | DispatchErrorResponse | unknown>>( "scheduler", "Internal server error", async (req, res) => { const { action, ...input } = req.body; // Route task actions to the user-task subsystem if (TASK_ACTIONS.has(action)) { await handleTaskAction(action, input, res); return; } // Calendar item actions (existing behavior) const items = loadItems(); const result = dispatchScheduler(action, items, input); respondWithDispatchResult(res, result, { shouldPersist: action !== SCHEDULER_ACTIONS.show, instructions: "Display the updated scheduler to the user.", persist: saveItems, }); }, ), ); async function handleTaskAction(action: string, input: Record, res: Response): Promise { log.info("scheduler", "task action: start", { action }); // Errors bubble up to the asyncHandler wrapper on the dispatch route, // which logs at `log.error("scheduler", "handler threw", …)` and // returns a generic 500. No inner try/catch needed here. if (action === SCHEDULER_ACTIONS.listTasks) { handleListTasks(res); return; } if (action === SCHEDULER_ACTIONS.createTask) { await handleCreateTask(input, res); return; } if (action === SCHEDULER_ACTIONS.deleteTask) { await handleDeleteTask(input, res); return; } if (action === SCHEDULER_ACTIONS.runTask) { await handleRunTask(input, res); return; } badRequest(res, `unknown task action: ${action}`); } function handleListTasks(res: Response): void { const tasks = loadUserTasks(); log.info("scheduler", "task action: listTasks ok", { tasks: tasks.length }); res.json({ uuid: makeUuid(), message: `${tasks.length} scheduled task(s) found.`, data: { tasks }, }); } async function handleCreateTask(input: Record, res: Response): Promise { const result = validateAndCreate(input); if (result.kind === "error") { log.warn("scheduler", "task action: createTask validation failed", { error: result.error }); badRequest(res, result.error); return; } const tasks = loadUserTasks(); tasks.push(result.task); await saveUserTasks(tasks); await refreshUserTasks(); log.info("scheduler", "task action: createTask ok", { id: result.task.id, name: result.task.name }); res.json({ uuid: makeUuid(), message: `Task "${result.task.name}" created and scheduled.`, data: { task: result.task }, }); } async function handleDeleteTask(input: Record, res: Response): Promise { const taskId = typeof input.id === "string" ? input.id : ""; const tasks = loadUserTasks(); const idx = tasks.findIndex((task) => task.id === taskId); const removed = tasks[idx]; if (removed === undefined) { log.warn("scheduler", "task action: deleteTask not found", { taskId }); notFound(res, `task not found: ${taskId}`); return; } const { name } = removed; tasks.splice(idx, 1); await saveUserTasks(tasks); await refreshUserTasks(); log.info("scheduler", "task action: deleteTask ok", { taskId, name }); res.json({ uuid: makeUuid(), message: `Task "${name}" deleted.`, data: { deleted: taskId }, }); } async function handleRunTask(input: Record, res: Response): Promise { const taskId = typeof input.id === "string" ? input.id : ""; const tasks = loadUserTasks(); const task = tasks.find((candidate) => candidate.id === taskId); if (!task) { notFound(res, `task not found: ${taskId}`); return; } const chatSessionId = makeUuid(); log.info("scheduler", "manual run via MCP", { name: task.name, chatSessionId, }); startChat({ message: task.prompt, roleId: task.roleId, chatSessionId, origin: SESSION_ORIGINS.scheduler, }).catch((err) => { log.error("scheduler", "manual run failed", { error: String(err), }); }); res.json({ uuid: makeUuid(), message: `Task "${task.name}" triggered.`, data: { triggered: taskId, chatSessionId }, }); } export default router;