@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
JavaScript
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