"""Direct Apple Notes automation for the mutable Reyna CLI.
The scripts are static JXA programs. User data is passed only as one JSON
argument to ``osascript`` so note content is never interpolated into source.
This deliberately stays outside the stable Swift host: Python-only CLI changes
do not require rebuilding or re-signing Reyna CLI.app.
"""
from __future__ import annotations
import json
import subprocess
from typing import Any, Callable, Optional, Sequence
class NotesAutomationError(RuntimeError):
"""Apple Notes could not complete the requested automation operation."""
Runner = Callable[..., Any]
_NOTES_LIST_SCRIPT = r'''function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("/System/Applications/Notes.app");
const query = (input.query || "").toLowerCase();
const folderName = input.folder || "";
const found = [];
const accounts = app.accounts();
for (let a = 0; a < accounts.length && found.length < input.limit; a++) {
const folders = accounts[a].folders();
for (let f = 0; f < folders.length && found.length < input.limit; f++) {
const folder = folders[f];
const currentFolder = String(folder.name());
if (folderName && currentFolder !== folderName) continue;
const notes = folder.notes();
for (let n = 0; n < notes.length && found.length < input.limit; n++) {
const note = notes[n];
let title = "";
let text = "";
try { title = String(note.name()); } catch (_) {}
try { text = String(note.plaintext()); } catch (_) {}
if (query && (title + "\n" + text).toLowerCase().indexOf(query) === -1) continue;
found.push({
id: String(note.id()),
title: title,
folder: currentFolder,
modifiedAt: note.modificationDate().toISOString(),
preview: input.includePreview ? text.slice(0, 180) : undefined
});
}
}
}
return JSON.stringify(found);
}'''
_NOTES_READ_SCRIPT = r'''function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("/System/Applications/Notes.app");
const accounts = app.accounts();
for (let a = 0; a < accounts.length; a++) {
const folders = accounts[a].folders();
for (let f = 0; f < folders.length; f++) {
const notes = folders[f].notes();
for (let n = 0; n < notes.length; n++) {
const note = notes[n];
if (String(note.id()) === input.id) {
return JSON.stringify({
id: String(note.id()),
title: String(note.name()),
folder: String(folders[f].name()),
bodyHtml: String(note.body()),
plaintext: String(note.plaintext()),
createdAt: note.creationDate().toISOString(),
modifiedAt: note.modificationDate().toISOString()
});
}
}
}
}
throw new Error("Note not found");
}'''
_NOTES_CREATE_SCRIPT = r'''function run(argv) {
const input = JSON.parse(argv[0]);
const app = Application("/System/Applications/Notes.app");
const escapeHtml = value => String(value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
const body = escapeHtml(input.body).replace(/\n/g, "
");
const html = "