-
Notifications
You must be signed in to change notification settings - Fork 2
Add reminders to notes #38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
amrelsagaei
merged 2 commits into
ae-issue-22-accessing-notes
from
ae-feature-20-add-reminder-for-notes
Apr 13, 2026
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| import { randomUUID } from "crypto"; | ||
|
|
||
| import type { SDK } from "caido:plugin"; | ||
| import type { Reminder, Result } from "shared"; | ||
| import { error, ok } from "shared"; | ||
|
|
||
| import { | ||
| createReminderSchema, | ||
| deleteReminderSchema, | ||
| dismissReminderSchema, | ||
| } from "../schemas/reminder"; | ||
| import { ensureProjectDirectory } from "../utils/fileSystem"; | ||
| import { readRemindersFile, writeRemindersFile } from "../utils/reminderFile"; | ||
|
|
||
| /** | ||
| * Get all reminders for the current project | ||
| */ | ||
| export async function getReminders(sdk: SDK): Promise<Result<Reminder[]>> { | ||
| try { | ||
| const projectIDResult = await ensureProjectDirectory(sdk); | ||
| if (projectIDResult.kind === "Error") { | ||
| return error(projectIDResult.error); | ||
| } | ||
|
|
||
| const reminders = readRemindersFile(projectIDResult.value); | ||
| return ok(reminders); | ||
| } catch (err) { | ||
| sdk.console.error(`Error getting reminders: ${err}`); | ||
| return error(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Create a new reminder | ||
| */ | ||
| export async function createReminder( | ||
| sdk: SDK, | ||
| notePath: string, | ||
| context: string, | ||
| reminderAt: string, | ||
| ): Promise<Result<Reminder>> { | ||
| try { | ||
| createReminderSchema.parse({ notePath, context, reminderAt }); | ||
|
|
||
| const parsedDate = new Date(reminderAt); | ||
| if (isNaN(parsedDate.getTime())) { | ||
| return error("Invalid reminder date"); | ||
| } | ||
| const normalizedReminderAt = parsedDate.toISOString(); | ||
|
|
||
| const projectIDResult = await ensureProjectDirectory(sdk); | ||
| if (projectIDResult.kind === "Error") { | ||
| return error(projectIDResult.error); | ||
| } | ||
|
|
||
| const projectID = projectIDResult.value; | ||
| const reminder: Reminder = { | ||
| id: randomUUID(), | ||
| notePath, | ||
| context, | ||
| reminderAt: normalizedReminderAt, | ||
| createdAt: new Date().toISOString(), | ||
| triggered: false, | ||
| dismissed: false, | ||
| }; | ||
|
|
||
| const reminders = readRemindersFile(projectID); | ||
| reminders.push(reminder); | ||
| writeRemindersFile(projectID, reminders); | ||
|
|
||
| return ok(reminder); | ||
| } catch (err) { | ||
| sdk.console.error(`Error creating reminder: ${err}`); | ||
| return error(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Delete a reminder by ID | ||
| */ | ||
| export async function deleteReminder( | ||
| sdk: SDK, | ||
| reminderId: string, | ||
| ): Promise<Result<boolean>> { | ||
| try { | ||
| deleteReminderSchema.parse({ reminderId }); | ||
|
|
||
| const projectIDResult = await ensureProjectDirectory(sdk); | ||
| if (projectIDResult.kind === "Error") { | ||
| return error(projectIDResult.error); | ||
| } | ||
|
|
||
| const projectID = projectIDResult.value; | ||
| const reminders = readRemindersFile(projectID); | ||
| const filtered = reminders.filter((r) => r.id !== reminderId); | ||
|
|
||
| if (filtered.length === reminders.length) { | ||
| return error(`Reminder not found: ${reminderId}`); | ||
| } | ||
|
|
||
| writeRemindersFile(projectID, filtered); | ||
| return ok(true); | ||
| } catch (err) { | ||
| sdk.console.error(`Error deleting reminder: ${err}`); | ||
| return error(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Dismiss a reminder (mark as acknowledged by user) | ||
| */ | ||
| export async function dismissReminder( | ||
| sdk: SDK, | ||
| reminderId: string, | ||
| ): Promise<Result<boolean>> { | ||
| try { | ||
| dismissReminderSchema.parse({ reminderId }); | ||
|
|
||
| const projectIDResult = await ensureProjectDirectory(sdk); | ||
| if (projectIDResult.kind === "Error") { | ||
| return error(projectIDResult.error); | ||
| } | ||
|
|
||
| const projectID = projectIDResult.value; | ||
| const reminders = readRemindersFile(projectID); | ||
| const target = reminders.find((r) => r.id === reminderId); | ||
|
|
||
| if (!target) { | ||
| return error(`Reminder not found: ${reminderId}`); | ||
| } | ||
|
|
||
| target.dismissed = true; | ||
| writeRemindersFile(projectID, reminders); | ||
| return ok(true); | ||
| } catch (err) { | ||
| sdk.console.error(`Error dismissing reminder: ${err}`); | ||
| return error(err instanceof Error ? err.message : String(err)); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import { z } from "zod"; | ||
|
|
||
| export const createReminderSchema = z.object({ | ||
| notePath: z.string().min(1), | ||
| context: z.string(), | ||
| reminderAt: z.string().datetime({ offset: true }), | ||
| }); | ||
|
|
||
| export const deleteReminderSchema = z.object({ | ||
| reminderId: z.string().min(1), | ||
| }); | ||
|
|
||
| export const dismissReminderSchema = z.object({ | ||
| reminderId: z.string().min(1), | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,7 @@ | ||
| import type { DefineEvents } from "caido:plugin"; | ||
| import type { Reminder } from "shared"; | ||
|
|
||
| export type BackendEvents = DefineEvents<{ | ||
| "notes++:projectChange": (projectId: string) => void; | ||
| "notes++:reminderDue": (reminder: Reminder) => void; | ||
| }>; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import * as fs from "fs"; | ||
| import path from "path"; | ||
|
|
||
| import type { Reminder } from "shared"; | ||
| import { z } from "zod"; | ||
|
|
||
| import { | ||
| createDirectory, | ||
| directoryExists, | ||
| fileExists, | ||
| toSystemPath, | ||
| } from "./fileSystem"; | ||
| import { getNoteRootPath } from "./paths"; | ||
|
|
||
| const REMINDERS_FILENAME = "reminders.json"; | ||
|
|
||
| const ReminderSchema = z.object({ | ||
| id: z.string(), | ||
| notePath: z.string(), | ||
| context: z.string(), | ||
| reminderAt: z.string(), | ||
| createdAt: z.string(), | ||
| triggered: z.boolean(), | ||
| dismissed: z.boolean(), | ||
| }); | ||
|
|
||
| const RemindersArraySchema = z.array(ReminderSchema); | ||
|
|
||
| export function getRemindersFilePath(projectID: string): string { | ||
| return path.join(getNoteRootPath(projectID), REMINDERS_FILENAME); | ||
| } | ||
|
|
||
| export function readRemindersFile(projectID: string): Reminder[] { | ||
| const filePath = getRemindersFilePath(projectID); | ||
|
|
||
| if (!fileExists(filePath)) { | ||
| return []; | ||
| } | ||
|
|
||
| try { | ||
| const raw = fs.readFileSync(toSystemPath(filePath), "utf8"); | ||
| const parsed = JSON.parse(raw); | ||
| const result = RemindersArraySchema.safeParse(parsed); | ||
| return result.success ? (result.data as Reminder[]) : []; | ||
| } catch { | ||
| return []; | ||
| } | ||
| } | ||
|
|
||
| export function writeRemindersFile( | ||
| projectID: string, | ||
| reminders: Reminder[], | ||
| ): void { | ||
| const filePath = getRemindersFilePath(projectID); | ||
| const dirPath = path.dirname(filePath); | ||
|
|
||
| if (!directoryExists(dirPath)) { | ||
| createDirectory(dirPath); | ||
| } | ||
|
|
||
| fs.writeFileSync(toSystemPath(filePath), JSON.stringify(reminders, null, 2)); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| import type { SDK } from "caido:plugin"; | ||
|
|
||
| import type { API } from "../index"; | ||
| import type { BackendEvents } from "../types/events"; | ||
|
|
||
| import { readRemindersFile, writeRemindersFile } from "./reminderFile"; | ||
|
|
||
| const CHECK_INTERVAL_MS = 30_000; | ||
|
|
||
| /** Periodically checks for due reminders and dispatches notification events. */ | ||
| export function startReminderTimer(sdk: SDK<API, BackendEvents>): () => void { | ||
| async function checkReminders() { | ||
| try { | ||
| const project = await sdk.projects.getCurrent(); | ||
| const projectID = project?.getId(); | ||
|
|
||
| if (!projectID) return; | ||
|
|
||
| const reminders = readRemindersFile(projectID); | ||
| const now = new Date(); | ||
| let changed = false; | ||
|
|
||
| for (const reminder of reminders) { | ||
| if (reminder.triggered || reminder.dismissed) continue; | ||
|
|
||
| const dueDate = new Date(reminder.reminderAt); | ||
| if (dueDate <= now) { | ||
| reminder.triggered = true; | ||
| changed = true; | ||
| sdk.api.send("notes++:reminderDue", reminder); | ||
| } | ||
| } | ||
|
|
||
| if (changed) { | ||
| writeRemindersFile(projectID, reminders); | ||
| } | ||
| } catch (err) { | ||
| sdk.console.error(`Error checking reminders: ${err}`); | ||
| } | ||
| } | ||
|
|
||
| checkReminders(); | ||
| const intervalId = setInterval(checkReminders, CHECK_INTERVAL_MS); | ||
|
|
||
| return () => clearInterval(intervalId); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate
reminderAtas a real timestamp before persisting it.This currently accepts any non-empty string, so bad values can be written and then parsed with
new Date(reminderAt)in the frontend. The result isInvalid Dateoutput or reminders that never transition to due/missed. Parse and normalize the timestamp here before writing the file.💡 Suggested change
try { createReminderSchema.parse({ notePath, context, reminderAt }); + const parsedReminderAt = new Date(reminderAt); + if (Number.isNaN(parsedReminderAt.getTime())) { + return error("Invalid reminderAt"); + } @@ const reminder: Reminder = { id: randomUUID(), notePath, context, - reminderAt, + reminderAt: parsedReminderAt.toISOString(), createdAt: new Date().toISOString(), triggered: false, dismissed: false,🤖 Prompt for AI Agents