#!/usr/bin/env python3 """ Reminder Manager for OpenClaw Discord Handles one-shot and recurring reminders via OpenClaw cron + SQLite """ import sqlite3 import os import sys import json import re from datetime import datetime, timedelta from pathlib import Path DB_PATH = os.path.expanduser("~/.openclaw/workspace/data/reminders.db") os.makedirs(os.path.dirname(DB_PATH), exist_ok=True) def init_db(): """Create the reminders database.""" conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''CREATE TABLE IF NOT EXISTS reminders ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, channel_id TEXT NOT NULL, message TEXT NOT NULL, remind_at TEXT NOT NULL, created_at TEXT DEFAULT CURRENT_TIMESTAMP, is_recurring INTEGER DEFAULT 0, recurrence_rule TEXT, cron_job_id TEXT, active INTEGER DEFAULT 1 )''') conn.commit() conn.close() def parse_time(time_str: str) -> datetime: """Parse various time formats into datetime.""" now = datetime.now() time_str = time_str.lower().strip() # Relative times: 20m, 2h, 1h30m match = re.match(r'^(\d+)m$', time_str) if match: return now + timedelta(minutes=int(match.group(1))) match = re.match(r'^(\d+)h$', time_str) if match: return now + timedelta(hours=int(match.group(1))) match = re.match(r'^(\d+)h(\d+)m$', time_str) if match: return now + timedelta(hours=int(match.group(1)), minutes=int(match.group(2))) # Tomorrow if time_str == 'tomorrow': return now + timedelta(days=1) # Tomorrow at time: tomorrow 9am, tomorrow 14:00 match = re.match(r'^tomorrow\s+([\d:]+)(am|pm)?$', time_str) if match: time_part = match.group(1) ampm = match.group(2) tomorrow = now + timedelta(days=1) if ':' in time_part: hour, minute = map(int, time_part.split(':')) else: hour, minute = int(time_part), 0 if ampm == 'pm' and hour != 12: hour += 12 if ampm == 'am' and hour == 12: hour = 0 return tomorrow.replace(hour=hour, minute=minute, second=0, microsecond=0) # Today at time: 9am, 14:00, 2:30pm match = re.match(r'^(\d{1,2}):(\d{2})(am|pm)?$', time_str) if match: hour = int(match.group(1)) minute = int(match.group(2)) ampm = match.group(3) if ampm == 'pm' and hour != 12: hour += 12 if ampm == 'am' and hour == 12: hour = 0 result = now.replace(hour=hour, minute=minute, second=0, microsecond=0) if result < now: result += timedelta(days=1) return result match = re.match(r'^(\d{1,2})(am|pm)$', time_str) if match: hour = int(match.group(1)) ampm = match.group(2) if ampm == 'pm' and hour != 12: hour += 12 if ampm == 'am' and hour == 12: hour = 0 result = now.replace(hour=hour, minute=0, second=0, microsecond=0) if result < now: result += timedelta(days=1) return result raise ValueError(f"Can't parse time: {time_str}") def add_reminder(user_id: str, channel_id: str, message: str, time_str: str) -> dict: """Add a new reminder and schedule it via OpenClaw cron.""" init_db() remind_at = parse_time(time_str) if remind_at < datetime.now(): return {"error": "Reminder time is in the past"} # Insert into DB conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('''INSERT INTO reminders (user_id, channel_id, message, remind_at, active) VALUES (?, ?, ?, ?, 1)''', (user_id, channel_id, message, remind_at.isoformat())) reminder_id = c.lastrowid conn.commit() conn.close() # Schedule via OpenClaw cron (will be handled by caller) return { "id": reminder_id, "message": message, "remind_at": remind_at.isoformat(), "user_id": user_id, "channel_id": channel_id } def list_reminders(user_id: str = None) -> list: """List active reminders for a user or all users.""" init_db() conn = sqlite3.connect(DB_PATH) c = conn.cursor() if user_id: c.execute('''SELECT id, message, remind_at, channel_id FROM reminders WHERE user_id = ? AND active = 1 AND remind_at > datetime('now') ORDER BY remind_at''', (user_id,)) else: c.execute('''SELECT id, message, remind_at, channel_id, user_id FROM reminders WHERE active = 1 AND remind_at > datetime('now') ORDER BY remind_at''') results = c.fetchall() conn.close() return results def delete_reminder(reminder_id: int, user_id: str = None) -> bool: """Delete/cancel a reminder.""" init_db() conn = sqlite3.connect(DB_PATH) c = conn.cursor() if user_id: c.execute('DELETE FROM reminders WHERE id = ? AND user_id = ?', (reminder_id, user_id)) else: c.execute('DELETE FROM reminders WHERE id = ?', (reminder_id,)) deleted = c.rowcount > 0 conn.commit() conn.close() return deleted def delete_past_reminders(): """Clean up old reminder entries.""" init_db() conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('DELETE FROM reminders WHERE remind_at < datetime("now", "-1 day")') conn.commit() conn.close() def cron_callback(reminder_id: int): """Called when a cron job fires - returns the reminder details.""" init_db() conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.execute('SELECT message, user_id, channel_id FROM reminders WHERE id = ?', (reminder_id,)) result = c.fetchone() if result: # Mark as inactive after firing c.execute('UPDATE reminders SET active = 0 WHERE id = ?', (reminder_id,)) conn.commit() conn.close() if result: return { "message": result[0], "user_id": result[1], "channel_id": result[2] } return None if __name__ == "__main__": command = sys.argv[1] if len(sys.argv) > 1 else "help" if command == "add": # Usage: reminder-manager.py add "user_id" "channel_id" "message" "time" user_id = sys.argv[2] channel_id = sys.argv[3] message = sys.argv[4] time_str = sys.argv[5] result = add_reminder(user_id, channel_id, message, time_str) print(json.dumps(result)) elif command == "list": user_id = sys.argv[2] if len(sys.argv) > 2 else None reminders = list_reminders(user_id) print(json.dumps([{ "id": r[0], "message": r[1], "remind_at": r[2], "channel_id": r[3], "user_id": r[4] if len(r) > 4 else None } for r in reminders])) elif command == "delete": reminder_id = int(sys.argv[2]) user_id = sys.argv[3] if len(sys.argv) > 3 else None deleted = delete_reminder(reminder_id, user_id) print(json.dumps({"deleted": deleted})) elif command == "callback": reminder_id = int(sys.argv[2]) result = cron_callback(reminder_id) print(json.dumps(result) if result else "null") elif command == "cleanup": delete_past_reminders() print("Cleanup complete") else: print("""Usage: reminder-manager.py add "user_id" "channel_id" "message" "time" reminder-manager.py list [user_id] reminder-manager.py delete [user_id] reminder-manager.py callback reminder-manager.py cleanup """)