#!/usr/bin/env python3 # /// script # requires-python = ">=3.11" # dependencies = [] # /// """SQLite storage layer for /remind skill. Schema, connection factory, and audit logging. """ from __future__ import annotations import sqlite3 from datetime import datetime, timezone from pathlib import Path SCHEMA = """ PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS reminders ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL CHECK(text <> ''), enabled INTEGER NOT NULL DEFAULT 1 CHECK(enabled IN (0, 1)), timezone TEXT NOT NULL DEFAULT 'Europe/Prague', created_at TEXT NOT NULL, updated_at TEXT NOT NULL, deleted_at TEXT ); CREATE TABLE IF NOT EXISTS schedule_at ( id INTEGER PRIMARY KEY AUTOINCREMENT, reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE, at_datetime TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS schedule_cron ( id INTEGER PRIMARY KEY AUTOINCREMENT, reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE, cron_expr TEXT NOT NULL CHECK(cron_expr <> '') ); CREATE TABLE IF NOT EXISTS schedule_random ( id INTEGER PRIMARY KEY AUTOINCREMENT, reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE, times_per_day INTEGER NOT NULL CHECK(times_per_day >= 1), window_start INTEGER NOT NULL CHECK(window_start >= 0 AND window_start < 1440), window_end INTEGER NOT NULL CHECK(window_end > 0 AND window_end <= 1440), days_filter TEXT, from_date TEXT, until_date TEXT, CHECK(window_start < window_end) ); CREATE TABLE IF NOT EXISTS reminder_fires ( id INTEGER PRIMARY KEY AUTOINCREMENT, reminder_id INTEGER NOT NULL REFERENCES reminders(id) ON DELETE CASCADE, schedule_id INTEGER NOT NULL, schedule_type TEXT NOT NULL CHECK(schedule_type IN ('at', 'cron', 'random')), fire_time TEXT NOT NULL, delivered_at TEXT, status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'delivered', 'failed')), error_message TEXT ); CREATE INDEX IF NOT EXISTS idx_reminders_text ON reminders(text); CREATE INDEX IF NOT EXISTS idx_at_datetime ON schedule_at(reminder_id, at_datetime); CREATE INDEX IF NOT EXISTS idx_cron_expr ON schedule_cron(reminder_id, cron_expr); CREATE INDEX IF NOT EXISTS idx_random_dates ON schedule_random(from_date, until_date); CREATE INDEX IF NOT EXISTS idx_fire_lookup ON reminder_fires( reminder_id, schedule_type, schedule_id, fire_time, status ); """ def get_db(path: Path) -> sqlite3.Connection: """Return a connection with WAL mode and foreign keys enabled.""" conn = sqlite3.connect(path, isolation_level=None) conn.execute("PRAGMA journal_mode = WAL") conn.execute("PRAGMA foreign_keys = ON") conn.row_factory = sqlite3.Row return conn def init_db(path: Path) -> None: """Create tables and indexes if they don't exist.""" path.parent.mkdir(parents=True, exist_ok=True) conn = get_db(path) try: conn.executescript(SCHEMA) finally: conn.close() def log_operation(operation: str, reminder_id: int | None, details: str) -> None: """Append an audit line to workspace/log/reminder.log.""" workspace = Path(__file__).resolve().parent.parent.parent.parent log_dir = workspace / "log" log_dir.mkdir(parents=True, exist_ok=True) log_file = log_dir / "reminder.log" ts = datetime.now(timezone.utc).isoformat(timespec="seconds") line = f"{ts} [{operation}]" if reminder_id is not None: line += f" id={reminder_id}" if details: line += f" {details}" with log_file.open("a", encoding="utf-8") as f: f.write(line + "\n")