UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

318 lines (317 loc) 9.42 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { existsSync, readFileSync } from "fs"; import { join } from "path"; import { homedir } from "os"; import { randomBytes } from "crypto"; import { writeFileSecure, ensureSecureDir } from "./secure-fs.js"; import { ScheduleStorageSchema, parseConfigSafe } from "./schemas.js"; import { getFrameDigestData, generateMobileDigest, loadSyncOptions } from "./whatsapp-sync.js"; import { sendNotification, loadSMSConfig } from "./sms-notify.js"; const STORAGE_PATH = join(homedir(), ".stackmemory", "whatsapp-schedules.json"); const DEFAULT_STORAGE = { schedules: [], lastChecked: (/* @__PURE__ */ new Date()).toISOString() }; let schedulerInterval = null; function loadStorage() { try { if (existsSync(STORAGE_PATH)) { const data = JSON.parse(readFileSync(STORAGE_PATH, "utf8")); return parseConfigSafe( ScheduleStorageSchema, data, DEFAULT_STORAGE, "whatsapp-schedules" ); } } catch { } return { ...DEFAULT_STORAGE, lastChecked: (/* @__PURE__ */ new Date()).toISOString() }; } function saveStorage(storage) { try { ensureSecureDir(join(homedir(), ".stackmemory")); writeFileSecure(STORAGE_PATH, JSON.stringify(storage, null, 2)); } catch { } } function generateScheduleId() { return randomBytes(6).toString("hex"); } function parseTime(time) { const match = time.match(/^(\d{2}):(\d{2})$/); if (!match) return null; const hours = parseInt(match[1], 10); const minutes = parseInt(match[2], 10); if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { return null; } return { hours, minutes }; } function calculateNextRun(config, fromDate) { const now = fromDate || /* @__PURE__ */ new Date(); switch (config.type) { case "daily": { const time = config.time ? parseTime(config.time) : { hours: 9, minutes: 0 }; if (!time) { throw new Error(`Invalid time format: ${config.time}`); } const next = new Date(now); next.setHours(time.hours, time.minutes, 0, 0); if (next <= now) { next.setDate(next.getDate() + 1); } return next; } case "hourly": { const next = new Date(now); next.setMinutes(0, 0, 0); next.setHours(next.getHours() + 1); return next; } case "interval": { const intervalMinutes = config.intervalMinutes || 60; const next = new Date(now.getTime() + intervalMinutes * 60 * 1e3); return next; } default: throw new Error(`Unknown schedule type: ${config.type}`); } } function isQuietHours() { const smsConfig = loadSMSConfig(); if (!smsConfig.quietHours?.enabled) { return false; } const now = /* @__PURE__ */ new Date(); const currentMinutes = now.getHours() * 60 + now.getMinutes(); const startTime = parseTime(smsConfig.quietHours.start); const endTime = parseTime(smsConfig.quietHours.end); if (!startTime || !endTime) { return false; } const startMinutes = startTime.hours * 60 + startTime.minutes; const endMinutes = endTime.hours * 60 + endTime.minutes; if (startMinutes > endMinutes) { return currentMinutes >= startMinutes || currentMinutes < endMinutes; } return currentMinutes >= startMinutes && currentMinutes < endMinutes; } function scheduleDigest(config) { const storage = loadStorage(); const id = generateScheduleId(); const nextRun = calculateNextRun(config); const schedule = { id, config, enabled: true, nextRun: nextRun.toISOString(), createdAt: (/* @__PURE__ */ new Date()).toISOString() }; storage.schedules.push(schedule); saveStorage(storage); console.log( `[whatsapp-scheduler] Created schedule ${id}, next run: ${nextRun.toISOString()}` ); return id; } function cancelSchedule(scheduleId) { const storage = loadStorage(); const initialLength = storage.schedules.length; storage.schedules = storage.schedules.filter((s) => s.id !== scheduleId); if (storage.schedules.length < initialLength) { saveStorage(storage); console.log(`[whatsapp-scheduler] Cancelled schedule ${scheduleId}`); return true; } return false; } function setScheduleEnabled(scheduleId, enabled) { const storage = loadStorage(); const schedule = storage.schedules.find((s) => s.id === scheduleId); if (!schedule) { return false; } schedule.enabled = enabled; if (enabled) { schedule.nextRun = calculateNextRun(schedule.config).toISOString(); } saveStorage(storage); return true; } function listSchedules() { const storage = loadStorage(); return storage.schedules; } function getSchedule(scheduleId) { const storage = loadStorage(); return storage.schedules.find((s) => s.id === scheduleId); } async function generateActivitySummary() { const data = await getFrameDigestData(); if (!data) { return null; } const options = loadSyncOptions(); return generateMobileDigest(data, options); } async function runScheduledDigest(scheduleId) { const storage = loadStorage(); const schedule = storage.schedules.find((s) => s.id === scheduleId); if (!schedule) { return { success: false, sent: false, error: `Schedule not found: ${scheduleId}` }; } if (!schedule.enabled) { return { success: false, sent: false, error: "Schedule is disabled" }; } if (schedule.config.quietHoursRespect && isQuietHours()) { return { success: true, sent: false, message: "Skipped due to quiet hours" }; } const digest = await generateActivitySummary(); if (!digest && !schedule.config.includeInactive) { schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString(); schedule.nextRun = calculateNextRun(schedule.config).toISOString(); saveStorage(storage); return { success: true, sent: false, message: "No activity to report" }; } const message = digest || "No recent activity. All systems idle."; const result = await sendNotification({ type: "custom", title: "Scheduled Digest", message, prompt: { type: "options", options: [ { key: "1", label: "Details", action: "stackmemory status" }, { key: "2", label: "Tasks", action: "stackmemory task list" } ] } }); schedule.lastRun = (/* @__PURE__ */ new Date()).toISOString(); schedule.nextRun = calculateNextRun(schedule.config).toISOString(); saveStorage(storage); if (result.success) { return { success: true, sent: true, message: `Digest sent (${message.length} chars)` }; } else { return { success: false, sent: false, error: result.error }; } } async function checkAndRunDueSchedules() { const storage = loadStorage(); const now = /* @__PURE__ */ new Date(); let ran = 0; let errors = 0; for (const schedule of storage.schedules) { if (!schedule.enabled || !schedule.nextRun) { continue; } const nextRun = new Date(schedule.nextRun); if (nextRun <= now) { console.log(`[whatsapp-scheduler] Running due schedule ${schedule.id}`); const result = await runScheduledDigest(schedule.id); if (result.success) { if (result.sent) { ran++; } } else { errors++; console.error( `[whatsapp-scheduler] Schedule ${schedule.id} failed: ${result.error}` ); } } } storage.lastChecked = now.toISOString(); saveStorage(storage); return { checked: storage.schedules.length, ran, errors }; } function startScheduler(checkIntervalMs = 6e4) { if (schedulerInterval) { console.log("[whatsapp-scheduler] Scheduler already running"); return; } console.log( `[whatsapp-scheduler] Starting scheduler (interval: ${checkIntervalMs}ms)` ); checkAndRunDueSchedules().catch(console.error); schedulerInterval = setInterval(() => { checkAndRunDueSchedules().catch(console.error); }, checkIntervalMs); } function stopScheduler() { if (schedulerInterval) { clearInterval(schedulerInterval); schedulerInterval = null; console.log("[whatsapp-scheduler] Scheduler stopped"); } } function isSchedulerRunning() { return schedulerInterval !== null; } function scheduleDailyDigest(time) { const parsed = parseTime(time); if (!parsed) { throw new Error( `Invalid time format: ${time}. Use HH:MM format (e.g., 09:00)` ); } return scheduleDigest({ type: "daily", time, includeInactive: false, quietHoursRespect: true }); } function scheduleHourlyDigest() { return scheduleDigest({ type: "hourly", includeInactive: false, quietHoursRespect: true }); } function scheduleIntervalDigest(intervalMinutes) { if (intervalMinutes < 5 || intervalMinutes > 1440) { throw new Error("Interval must be between 5 and 1440 minutes"); } return scheduleDigest({ type: "interval", intervalMinutes, includeInactive: false, quietHoursRespect: true }); } export { cancelSchedule, checkAndRunDueSchedules, getSchedule, isSchedulerRunning, listSchedules, runScheduledDigest, scheduleDailyDigest, scheduleDigest, scheduleHourlyDigest, scheduleIntervalDigest, setScheduleEnabled, startScheduler, stopScheduler }; //# sourceMappingURL=whatsapp-scheduler.js.map