From 7e3eda255b2823cb1db52816c5f5ff0195b0f73d Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Mon, 24 Aug 2026 22:53:36 -0400 Subject: [PATCH 01/12] Update apple-notes extension - Enhance error handling and code formatting across note management tools - Implement AI tools for note management: add delete, restore, move, append, and list folders; enhance search with tag filtering. --- extensions/apple-notes/CHANGELOG.md | 6 + extensions/apple-notes/package.json | 185 +++++++++++++++++- extensions/apple-notes/src/ai.ts | 36 +++- extensions/apple-notes/src/api/applescript.ts | 60 ++++++ extensions/apple-notes/src/helpers.ts | 4 + .../apple-notes/src/tools/append-to-note.ts | 21 ++ .../apple-notes/src/tools/delete-note.ts | 23 +++ .../apple-notes/src/tools/list-folders.ts | 10 + extensions/apple-notes/src/tools/move-note.ts | 24 +++ .../apple-notes/src/tools/restore-note.ts | 12 ++ .../apple-notes/src/tools/search-notes.ts | 8 +- 11 files changed, 378 insertions(+), 11 deletions(-) create mode 100644 extensions/apple-notes/src/tools/append-to-note.ts create mode 100644 extensions/apple-notes/src/tools/delete-note.ts create mode 100644 extensions/apple-notes/src/tools/list-folders.ts create mode 100644 extensions/apple-notes/src/tools/move-note.ts create mode 100644 extensions/apple-notes/src/tools/restore-note.ts diff --git a/extensions/apple-notes/CHANGELOG.md b/extensions/apple-notes/CHANGELOG.md index 6901df1ed8d..312e57ef9c5 100644 --- a/extensions/apple-notes/CHANGELOG.md +++ b/extensions/apple-notes/CHANGELOG.md @@ -1,5 +1,11 @@ # Apple Notes Changelog +## [AI Extension Improvements] - {PR_MERGE_DATE} + +- Add `delete-note`, `restore-note`, `move-note`, `append-to-note`, and `list-folders` AI tools +- Add tag filtering to the `search-notes` AI tool +- Let the "AI Note" command append AI-generated content to an existing note via a new optional `note` argument + ## [Bug Fixes] - 2026-06-07 - Fix tags not rendering in note detail view diff --git a/extensions/apple-notes/package.json b/extensions/apple-notes/package.json index b0d2e69b254..a9897d5cae1 100644 --- a/extensions/apple-notes/package.json +++ b/extensions/apple-notes/package.json @@ -20,7 +20,8 @@ "andreaselia", "fhf1121", "charlesharries", - "janosorcsik" + "janosorcsik", + "bitforger" ], "pastContributors": [ "thomaslombart" @@ -72,7 +73,7 @@ "name": "ai", "title": "AI Note", "subtitle": "Apple Notes", - "description": "Create a new note filled with AI in your Apple notes.", + "description": "Create a new note filled with AI in your Apple notes, or add to an existing one.", "mode": "no-view", "arguments": [ { @@ -85,6 +86,11 @@ "name": "instructions", "type": "text", "placeholder": "Additional instructions" + }, + { + "name": "note", + "type": "text", + "placeholder": "Existing note (optional)" } ] }, @@ -150,10 +156,35 @@ "name": "update-note", "title": "Update Note", "description": "Update the content of a specific note in Apple Notes." + }, + { + "name": "append-to-note", + "title": "Append to Note", + "description": "Add content to the end of a specific note in Apple Notes, keeping its existing content." + }, + { + "name": "delete-note", + "title": "Delete Note", + "description": "Delete a specific note in Apple Notes." + }, + { + "name": "restore-note", + "title": "Restore Note", + "description": "Restore a previously deleted note in Apple Notes." + }, + { + "name": "move-note", + "title": "Move Note", + "description": "Move a specific note to a different folder in Apple Notes." + }, + { + "name": "list-folders", + "title": "List Folders", + "description": "List the available folders in Apple Notes." } ], "ai": { - "instructions": "- Always format note titles as Markdown links using the note's URL.\\n- If you can't access the Apple Notes database, tell the user he needs to give Raycast the full disk access permission in the system settings. Be detailed about the steps.\\n- When calling get-note-content or update-note from search-notes results, use the note's id field and not UUID.", + "instructions": "- Always format note titles as Markdown links using the note's URL.\\n- If you can't access the Apple Notes database, tell the user he needs to give Raycast the full disk access permission in the system settings. Be detailed about the steps.\\n- When calling get-note-content, update-note, append-to-note, delete-note, or move-note from search-notes results, use the note's id field and not UUID.\\n- Use update-note to replace a note's whole content, and append-to-note to add new content while keeping what's already there.\\n- Before calling move-note, call list-folders to find a valid folder name; move-note will fail if the folder doesn't exist.\\n- delete-note is destructive and asks the user to confirm; if a note was deleted by mistake, restore-note can bring it back.", "evals": [ { "input": "@apple-notes what's on my grocery list", @@ -363,6 +394,152 @@ } } ] + }, + { + "input": "@apple-notes delete my grocery list note", + "mocks": { + "search-notes": [ + { + "id": "grocery-list", + "folder": "Home", + "modifiedAt": "2025-01-21 13:15:50", + "snippet": "- Bananas - Carrots", + "title": "Grocery list" + } + ], + "delete-note": "Successfully called \"delete-note\"" + }, + "expected": [ + { + "callsTool": "search-notes" + }, + { + "callsTool": { + "name": "delete-note", + "arguments": { + "noteId": "grocery-list" + } + } + } + ] + }, + { + "input": "@apple-notes what folders do I have in Apple Notes?", + "mocks": { + "list-folders": [ + { + "account": "iCloud", + "folder": "Notes" + }, + { + "account": "iCloud", + "folder": "Work" + }, + { + "account": "iCloud", + "folder": "Recipes" + } + ] + }, + "expected": [ + { + "callsTool": "list-folders" + }, + { + "meetsCriteria": "Returns a list of 3 folders." + } + ] + }, + { + "input": "@apple-notes move my grocery list note to the Work folder", + "mocks": { + "search-notes": [ + { + "id": "grocery-list", + "folder": "Home", + "modifiedAt": "2025-01-21 13:15:50", + "snippet": "- Bananas - Carrots", + "title": "Grocery list" + } + ], + "list-folders": [ + { + "account": "iCloud", + "folder": "Home" + }, + { + "account": "iCloud", + "folder": "Work" + } + ], + "move-note": "Successfully called \"move-note\"" + }, + "expected": [ + { + "callsTool": "search-notes" + }, + { + "callsTool": { + "name": "move-note", + "arguments": { + "noteId": "grocery-list", + "folderName": "Work" + } + } + } + ] + }, + { + "input": "@apple-notes add milk to my grocery list without removing what's already there", + "mocks": { + "search-notes": [ + { + "id": "grocery-list", + "folder": "Home", + "modifiedAt": "2025-01-21 13:15:50", + "snippet": "- Bananas - Carrots", + "title": "Grocery list" + } + ], + "append-to-note": "Successfully called \"append-to-note\"" + }, + "expected": [ + { + "callsTool": "search-notes" + }, + { + "callsTool": { + "name": "append-to-note", + "arguments": { + "noteId": "grocery-list" + } + } + } + ] + }, + { + "input": "@apple-notes find my work notes tagged urgent", + "mocks": { + "search-notes": [ + { + "id": "project-milestones", + "folder": "Work", + "modifiedAt": "2025-01-19 16:45:22", + "snippet": "- Design review - User testing - Launch prep", + "title": "Q1 Project Milestones" + } + ] + }, + "expected": [ + { + "callsTool": { + "name": "search-notes", + "arguments": { + "tags": "urgent" + } + } + } + ] } ] }, @@ -483,4 +660,4 @@ "platforms": [ "macOS" ] -} +} \ No newline at end of file diff --git a/extensions/apple-notes/src/ai.ts b/extensions/apple-notes/src/ai.ts index ae9a167fa1e..15628ab443f 100644 --- a/extensions/apple-notes/src/ai.ts +++ b/extensions/apple-notes/src/ai.ts @@ -1,16 +1,33 @@ import { AI, closeMainWindow, LaunchProps, showToast, Toast } from "@raycast/api"; import { showFailureToast } from "@raycast/utils"; -import { createNote } from "./api/applescript"; +import { appendNoteBody, createNote } from "./api/applescript"; +import { getNotes } from "./api/getNotes"; export default async (props: LaunchProps<{ arguments: Arguments.Ai }>) => { await closeMainWindow(); - await showToast({ style: Toast.Style.Animated, title: "Creating a note" }); - const text = props.fallbackText || props.arguments.text; - const instructions = props.arguments.instructions; + const noteQuery = props.arguments.note?.trim(); + + let targetNote: Awaited>[number] | undefined; + if (noteQuery) { + await showToast({ style: Toast.Style.Animated, title: "Looking for note" }); + const matches = await getNotes(50, [], noteQuery); + targetNote = matches.find((note) => note.title.toLowerCase() === noteQuery.toLowerCase()) ?? matches[0]; + if (!targetNote) { + await showFailureToast(new Error(`No note matching "${noteQuery}" was found.`), { + title: "Could not find note", + }); + return; + } + } + + await showToast({ + style: Toast.Style.Animated, + title: targetNote ? "Adding to note" : "Creating a note", + }); try { const result = await AI.ask( @@ -25,8 +42,15 @@ Follow these instructions: ${instructions ? `- ${instructions}` : ""} `, ); - await createNote(result); + + if (targetNote) { + await appendNoteBody(targetNote.id, result); + } else { + await createNote(result); + } } catch (error) { - await showFailureToast(error, { title: "Could not create a new note." }); + await showFailureToast(error, { + title: targetNote ? "Could not add to the note." : "Could not create a new note.", + }); } }; diff --git a/extensions/apple-notes/src/api/applescript.ts b/extensions/apple-notes/src/api/applescript.ts index adb69dbfa58..a935b9e1cda 100644 --- a/extensions/apple-notes/src/api/applescript.ts +++ b/extensions/apple-notes/src/api/applescript.ts @@ -101,3 +101,63 @@ export async function getSelectedNote() { end tell `); } + +export async function appendNoteBody(id: string, content: string) { + return runAppleScript( + ` + tell application "Notes" + set theNote to note id "${escapeDoubleQuotes(id)}" + set body of theNote to (body of theNote) & "${escapeDoubleQuotes(content)}" + end tell + `, + { timeout: 30_000 }, + ); +} + +export async function moveNoteToFolder(id: string, folderName: string, accountName?: string) { + const escapedFolderName = escapeDoubleQuotes(folderName); + // Without an explicit account, search every account for a matching folder name. + const findFolder = accountName + ? `set theFolder to folder "${escapedFolderName}" of account "${escapeDoubleQuotes(accountName)}"` + : ` + set theFolder to missing value + repeat with acc in accounts + try + set theFolder to folder "${escapedFolderName}" of acc + exit repeat + end try + end repeat + if theFolder is missing value then error "Folder \\"${escapedFolderName}\\" not found" + `; + + return runAppleScript( + ` + tell application "Notes" + set theNote to note id "${escapeDoubleQuotes(id)}" + ${findFolder} + move theNote to theFolder + end tell + `, + { timeout: 30_000 }, + ); +} + +export async function getFolders() { + return runAppleScript( + ` + tell application "Notes" + set output to {} + repeat with acc in accounts + repeat with fld in folders of acc + copy ((name of acc) & "|" & (name of fld)) to end of output + end repeat + end repeat + set AppleScript's text item delimiters to linefeed + set resultText to output as text + set AppleScript's text item delimiters to "" + return resultText + end tell + `, + { timeout: 30_000 }, + ); +} diff --git a/extensions/apple-notes/src/helpers.ts b/extensions/apple-notes/src/helpers.ts index 09ffa1c77a9..55963482b63 100644 --- a/extensions/apple-notes/src/helpers.ts +++ b/extensions/apple-notes/src/helpers.ts @@ -71,6 +71,10 @@ export function getOpenNoteURL(uuid: string) { } export async function resolveAppleNoteId(noteId: string): Promise { + if (!noteId) { + throw new Error('A noteId is required. Use the "id" field returned by search-notes.'); + } + if (noteId.startsWith("x-coredata://")) { return noteId; } diff --git a/extensions/apple-notes/src/tools/append-to-note.ts b/extensions/apple-notes/src/tools/append-to-note.ts new file mode 100644 index 00000000000..1bc8de23724 --- /dev/null +++ b/extensions/apple-notes/src/tools/append-to-note.ts @@ -0,0 +1,21 @@ +import { appendNoteBody } from "../api/applescript"; +import { resolveAppleNoteId } from "../helpers"; + +type Input = { + /** The note identifier. Use the "id" value from search-notes when possible. */ + noteId: string; + /** + * The content to append to the note, formatted as HTML, so that it can be pasted into Apple Notes. + * + * - Don't repeat the existing content of the note, only provide the new content to add. + * - Use the same language as the existing note. + * - Break the content into paragraphs with line breaks. + * - Don't use Markdown links (e.g. [Link](https://example.com)), use HTML links (e.g. Link). + */ + content: string; +}; + +export default async function (input: Input) { + const noteId = await resolveAppleNoteId(input.noteId); + return appendNoteBody(noteId, input.content); +} diff --git a/extensions/apple-notes/src/tools/delete-note.ts b/extensions/apple-notes/src/tools/delete-note.ts new file mode 100644 index 00000000000..9e17caa4e8e --- /dev/null +++ b/extensions/apple-notes/src/tools/delete-note.ts @@ -0,0 +1,23 @@ +import { Action, Tool } from "@raycast/api"; + +import { deleteNoteById } from "../api/applescript"; +import { resolveAppleNoteId } from "../helpers"; + +type Input = { + /** The note identifier. Use the "id" value from search-notes when possible. */ + noteId: string; + /** The title of the note, used to display in the confirmation dialog. */ + noteTitle?: string; +}; + +export default async function (input: Input) { + const noteId = await resolveAppleNoteId(input.noteId); + return deleteNoteById(noteId); +} + +export const confirmation: Tool.Confirmation = async (input) => { + return { + style: Action.Style.Destructive, + message: `Are you sure you want to delete "${input.noteTitle ?? input.noteId}"?`, + }; +}; diff --git a/extensions/apple-notes/src/tools/list-folders.ts b/extensions/apple-notes/src/tools/list-folders.ts new file mode 100644 index 00000000000..92c4ba91a64 --- /dev/null +++ b/extensions/apple-notes/src/tools/list-folders.ts @@ -0,0 +1,10 @@ +import { getFolders } from "../api/applescript"; + +export default async function () { + const output = await getFolders(); + return output + .split("\n") + .map((line) => line.split("|")) + .filter(([account, folder]) => account && folder) + .map(([account, folder]) => ({ account, folder })); +} diff --git a/extensions/apple-notes/src/tools/move-note.ts b/extensions/apple-notes/src/tools/move-note.ts new file mode 100644 index 00000000000..e9596b21791 --- /dev/null +++ b/extensions/apple-notes/src/tools/move-note.ts @@ -0,0 +1,24 @@ +import { Tool } from "@raycast/api"; + +import { moveNoteToFolder } from "../api/applescript"; +import { resolveAppleNoteId } from "../helpers"; + +type Input = { + /** The note identifier. Use the "id" value from search-notes when possible. */ + noteId: string; + /** The name of the destination folder. Use list-folders to find a valid folder name. */ + folderName: string; + /** The name of the account that owns the destination folder, if known. */ + accountName?: string; +}; + +export default async function (input: Input) { + const noteId = await resolveAppleNoteId(input.noteId); + return moveNoteToFolder(noteId, input.folderName, input.accountName); +} + +export const confirmation: Tool.Confirmation = async (input) => { + return { + info: [{ name: "Destination folder", value: input.folderName }], + }; +}; diff --git a/extensions/apple-notes/src/tools/restore-note.ts b/extensions/apple-notes/src/tools/restore-note.ts new file mode 100644 index 00000000000..334a624b160 --- /dev/null +++ b/extensions/apple-notes/src/tools/restore-note.ts @@ -0,0 +1,12 @@ +import { restoreNoteById } from "../api/applescript"; +import { resolveAppleNoteId } from "../helpers"; + +type Input = { + /** The note identifier. Use the "id" value from search-notes when possible. */ + noteId: string; +}; + +export default async function (input: Input) { + const noteId = await resolveAppleNoteId(input.noteId); + return restoreNoteById(noteId); +} diff --git a/extensions/apple-notes/src/tools/search-notes.ts b/extensions/apple-notes/src/tools/search-notes.ts index d31245a4488..c938e496243 100644 --- a/extensions/apple-notes/src/tools/search-notes.ts +++ b/extensions/apple-notes/src/tools/search-notes.ts @@ -5,11 +5,17 @@ import { getNotes } from "../api/getNotes"; type Input = { /** Optional text query used to search note titles and snippets. */ searchText?: string; + /** Optional comma-separated list of tags to filter notes by, e.g. "work,urgent". A note must have ALL given tags. */ + tags?: string; }; export default async function (input: Input = {}) { const { maxQueryResults } = getPreferenceValues(); const max = parseInt(maxQueryResults, 10) || 250; - const notes = await getNotes(max, [], input.searchText); + const tags = input.tags + ?.split(",") + .map((tag) => tag.trim()) + .filter(Boolean); + const notes = await getNotes(max, tags ?? [], input.searchText); return notes; } From c60a9b0c4fb7cba08f9cbe77ada13a7d67b51e16 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 11:31:15 -0400 Subject: [PATCH 02/12] Enhance note retrieval logic to improve accuracy and handle ambiguous titles; update delete confirmation message for clarity. --- extensions/apple-notes/src/ai.ts | 30 +++++++++++++++++-- extensions/apple-notes/src/api/applescript.ts | 20 ++++++++++--- .../apple-notes/src/tools/delete-note.ts | 2 +- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/extensions/apple-notes/src/ai.ts b/extensions/apple-notes/src/ai.ts index 15628ab443f..2e7bae7004f 100644 --- a/extensions/apple-notes/src/ai.ts +++ b/extensions/apple-notes/src/ai.ts @@ -14,8 +14,34 @@ export default async (props: LaunchProps<{ arguments: Arguments.Ai }>) => { let targetNote: Awaited>[number] | undefined; if (noteQuery) { await showToast({ style: Toast.Style.Animated, title: "Looking for note" }); - const matches = await getNotes(50, [], noteQuery); - targetNote = matches.find((note) => note.title.toLowerCase() === noteQuery.toLowerCase()) ?? matches[0]; + const findExactMatches = (notes: Awaited>) => { + const normalizedQuery = noteQuery.toLowerCase(); + return notes.filter((note) => note.title.trim().toLowerCase() === normalizedQuery); + }; + + const initialMatches = await getNotes(50, [], noteQuery); + let exactMatches = findExactMatches(initialMatches); + + // Retry with a larger window so an older exact-title match is not missed. + if (exactMatches.length === 0 && initialMatches.length === 50) { + const expandedMatches = await getNotes(500, [], noteQuery); + exactMatches = findExactMatches(expandedMatches); + } + + if (exactMatches.length === 1) { + targetNote = exactMatches[0]; + } + + if (exactMatches.length > 1) { + await showFailureToast( + new Error(`Multiple notes titled "${noteQuery}" were found. Please use a more specific title.`), + { + title: "Note title is ambiguous", + }, + ); + return; + } + if (!targetNote) { await showFailureToast(new Error(`No note matching "${noteQuery}" was found.`), { title: "Could not find note", diff --git a/extensions/apple-notes/src/api/applescript.ts b/extensions/apple-notes/src/api/applescript.ts index a935b9e1cda..0e03c9aae58 100644 --- a/extensions/apple-notes/src/api/applescript.ts +++ b/extensions/apple-notes/src/api/applescript.ts @@ -116,18 +116,30 @@ export async function appendNoteBody(id: string, content: string) { export async function moveNoteToFolder(id: string, folderName: string, accountName?: string) { const escapedFolderName = escapeDoubleQuotes(folderName); - // Without an explicit account, search every account for a matching folder name. + // Without an explicit account, require a single matching folder across accounts. const findFolder = accountName ? `set theFolder to folder "${escapedFolderName}" of account "${escapeDoubleQuotes(accountName)}"` : ` set theFolder to missing value + set matchingAccountNames to {} repeat with acc in accounts try - set theFolder to folder "${escapedFolderName}" of acc - exit repeat + set candidateFolder to folder "${escapedFolderName}" of acc + copy (name of acc) to end of matchingAccountNames + if theFolder is missing value then + set theFolder to candidateFolder + end if end try end repeat - if theFolder is missing value then error "Folder \\"${escapedFolderName}\\" not found" + if (count of matchingAccountNames) is 0 then + error "Folder \\"${escapedFolderName}\\" not found" + end if + if (count of matchingAccountNames) > 1 then + set AppleScript's text item delimiters to ", " + set matchingAccountsText to matchingAccountNames as text + set AppleScript's text item delimiters to "" + error "Folder \\"${escapedFolderName}\\" exists in multiple accounts (" & matchingAccountsText & "). Provide accountName." + end if `; return runAppleScript( diff --git a/extensions/apple-notes/src/tools/delete-note.ts b/extensions/apple-notes/src/tools/delete-note.ts index 9e17caa4e8e..c66f24754f8 100644 --- a/extensions/apple-notes/src/tools/delete-note.ts +++ b/extensions/apple-notes/src/tools/delete-note.ts @@ -18,6 +18,6 @@ export default async function (input: Input) { export const confirmation: Tool.Confirmation = async (input) => { return { style: Action.Style.Destructive, - message: `Are you sure you want to delete "${input.noteTitle ?? input.noteId}"?`, + message: `Are you sure you want to delete note "${input.noteId}"?`, }; }; From c2d2fb584f1d501d75e5997beb87d4b512693224 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 13:41:42 -0400 Subject: [PATCH 03/12] Enhance delete note functionality: add SQL query for note title retrieval and improve confirmation message display. --- .../apple-notes/src/tools/delete-note.ts | 51 ++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/extensions/apple-notes/src/tools/delete-note.ts b/extensions/apple-notes/src/tools/delete-note.ts index c66f24754f8..2b61571696a 100644 --- a/extensions/apple-notes/src/tools/delete-note.ts +++ b/extensions/apple-notes/src/tools/delete-note.ts @@ -1,7 +1,8 @@ import { Action, Tool } from "@raycast/api"; +import { executeSQL } from "@raycast/utils"; import { deleteNoteById } from "../api/applescript"; -import { resolveAppleNoteId } from "../helpers"; +import { escapeSQLString, NOTES_DB, resolveAppleNoteId } from "../helpers"; type Input = { /** The note identifier. Use the "id" value from search-notes when possible. */ @@ -10,14 +11,60 @@ type Input = { noteTitle?: string; }; +type NoteTitleRow = { + title: string | null; +}; + +function shortenNoteId(noteId: string) { + if (noteId.length <= 48) { + return noteId; + } + + return `${noteId.slice(0, 30)}...${noteId.slice(-12)}`; +} + +async function getNoteTitleByResolvedId(resolvedNoteId: string): Promise { + const escapedResolvedNoteId = escapeSQLString(resolvedNoteId); + const rows = await executeSQL( + NOTES_DB, + ` + SELECT + note.ztitle1 AS title + FROM + ziccloudsyncingobject AS note + LEFT JOIN z_metadata AS zmd ON 1=1 + WHERE + ('x-coredata://' || zmd.z_uuid || '/ICNote/p' || note.z_pk) = '${escapedResolvedNoteId}' + LIMIT 1 + `, + ); + + const title = rows?.[0]?.title?.trim(); + return title || undefined; +} + export default async function (input: Input) { const noteId = await resolveAppleNoteId(input.noteId); return deleteNoteById(noteId); } export const confirmation: Tool.Confirmation = async (input) => { + let resolvedNoteId = input.noteId; + let resolvedTitle: string | undefined; + + try { + resolvedNoteId = await resolveAppleNoteId(input.noteId); + resolvedTitle = await getNoteTitleByResolvedId(resolvedNoteId); + } catch { + // Fall back to ID-only confirmation when identity lookup fails. + } + + const displayId = shortenNoteId(resolvedNoteId); + return { style: Action.Style.Destructive, - message: `Are you sure you want to delete note "${input.noteId}"?`, + message: resolvedTitle + ? `Are you sure you want to delete note "${resolvedTitle}" (ID: ${displayId})?` + : `Are you sure you want to delete note ID "${displayId}"?`, }; }; From 758b081bd5dbd2f07019278cc99ab8e8abb604cf Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 14:28:57 -0400 Subject: [PATCH 04/12] Enhance confirmation message in move note tool to include destination account information if provided. --- extensions/apple-notes/src/tools/move-note.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/extensions/apple-notes/src/tools/move-note.ts b/extensions/apple-notes/src/tools/move-note.ts index e9596b21791..18ac2457ca1 100644 --- a/extensions/apple-notes/src/tools/move-note.ts +++ b/extensions/apple-notes/src/tools/move-note.ts @@ -19,6 +19,9 @@ export default async function (input: Input) { export const confirmation: Tool.Confirmation = async (input) => { return { - info: [{ name: "Destination folder", value: input.folderName }], + info: [ + { name: "Destination folder", value: input.folderName }, + ...(input.accountName ? [{ name: "Destination account", value: input.accountName }] : []), + ], }; }; From 91ea189b8413d23a61b3a9ca703da65eb1213c04 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 14:58:44 -0400 Subject: [PATCH 05/12] Refactor note retrieval logic to improve exact title matching and enhance SQL query efficiency. --- extensions/apple-notes/src/ai.ts | 14 ++------------ extensions/apple-notes/src/api/getNotes.ts | 12 ++++++++++-- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/extensions/apple-notes/src/ai.ts b/extensions/apple-notes/src/ai.ts index 2e7bae7004f..c85167cfc85 100644 --- a/extensions/apple-notes/src/ai.ts +++ b/extensions/apple-notes/src/ai.ts @@ -14,19 +14,9 @@ export default async (props: LaunchProps<{ arguments: Arguments.Ai }>) => { let targetNote: Awaited>[number] | undefined; if (noteQuery) { await showToast({ style: Toast.Style.Animated, title: "Looking for note" }); - const findExactMatches = (notes: Awaited>) => { - const normalizedQuery = noteQuery.toLowerCase(); - return notes.filter((note) => note.title.trim().toLowerCase() === normalizedQuery); - }; - const initialMatches = await getNotes(50, [], noteQuery); - let exactMatches = findExactMatches(initialMatches); - - // Retry with a larger window so an older exact-title match is not missed. - if (exactMatches.length === 0 && initialMatches.length === 50) { - const expandedMatches = await getNotes(500, [], noteQuery); - exactMatches = findExactMatches(expandedMatches); - } + // Exact title match is filtered in SQL, so it can't be missed regardless of how many notes exist. + const exactMatches = await getNotes(10, [], noteQuery, true); if (exactMatches.length === 1) { targetNote = exactMatches[0]; diff --git a/extensions/apple-notes/src/api/getNotes.ts b/extensions/apple-notes/src/api/getNotes.ts index 8d7d144ca89..2b4d3f2f631 100644 --- a/extensions/apple-notes/src/api/getNotes.ts +++ b/extensions/apple-notes/src/api/getNotes.ts @@ -2,10 +2,18 @@ import { executeSQL } from "@raycast/utils"; import { escapeSQLString, getOpenNoteURL, NOTES_DB, Link, Backlink, Tag, NoteItem } from "../helpers"; -export async function getNotes(maxQueryResults: number, filterByTags: string[] = [], searchText?: string) { +export async function getNotes( + maxQueryResults: number, + filterByTags: string[] = [], + searchText?: string, + exactTitleMatch = false, +) { const trimmedSearchText = searchText?.trim(); + // Exact-title matches are filtered in SQL (before LIMIT) so a match can't be pushed out of the window by recency. const searchFilter = trimmedSearchText - ? ` AND ( + ? exactTitleMatch + ? ` AND LOWER(TRIM(note.ztitle1)) = LOWER('${escapeSQLString(trimmedSearchText)}')` + : ` AND ( note.ztitle1 LIKE '%${escapeSQLString(trimmedSearchText)}%' OR note.zsnippet LIKE '%${escapeSQLString(trimmedSearchText)}%' )` From d333e53d3baf34a5f8257fe63ca91669be6ea1f9 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 15:48:27 -0400 Subject: [PATCH 06/12] Refactor confirmation logic to ensure resolution failures propagate and improve title lookup error handling. --- extensions/apple-notes/src/tools/delete-note.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/extensions/apple-notes/src/tools/delete-note.ts b/extensions/apple-notes/src/tools/delete-note.ts index 2b61571696a..f96fb5a785e 100644 --- a/extensions/apple-notes/src/tools/delete-note.ts +++ b/extensions/apple-notes/src/tools/delete-note.ts @@ -49,14 +49,16 @@ export default async function (input: Input) { } export const confirmation: Tool.Confirmation = async (input) => { - let resolvedNoteId = input.noteId; - let resolvedTitle: string | undefined; + // Resolution failures must propagate: falling back to the raw input id here would let the + // confirmation show an unresolved identifier while execution independently re-resolves and + // deletes whatever note that resolves to. + const resolvedNoteId = await resolveAppleNoteId(input.noteId); + let resolvedTitle: string | undefined; try { - resolvedNoteId = await resolveAppleNoteId(input.noteId); resolvedTitle = await getNoteTitleByResolvedId(resolvedNoteId); } catch { - // Fall back to ID-only confirmation when identity lookup fails. + // Fall back to ID-only confirmation when the title lookup fails. } const displayId = shortenNoteId(resolvedNoteId); From e168ba1a29decd05b83a616076ac828b74b2a0b9 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 16:43:28 -0400 Subject: [PATCH 07/12] Enhance documentation for accountName field in move-note tool input type to clarify its requirement when multiple accounts exist. --- extensions/apple-notes/package.json | 2 +- extensions/apple-notes/src/tools/move-note.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/apple-notes/package.json b/extensions/apple-notes/package.json index a9897d5cae1..6f4679d672c 100644 --- a/extensions/apple-notes/package.json +++ b/extensions/apple-notes/package.json @@ -184,7 +184,7 @@ } ], "ai": { - "instructions": "- Always format note titles as Markdown links using the note's URL.\\n- If you can't access the Apple Notes database, tell the user he needs to give Raycast the full disk access permission in the system settings. Be detailed about the steps.\\n- When calling get-note-content, update-note, append-to-note, delete-note, or move-note from search-notes results, use the note's id field and not UUID.\\n- Use update-note to replace a note's whole content, and append-to-note to add new content while keeping what's already there.\\n- Before calling move-note, call list-folders to find a valid folder name; move-note will fail if the folder doesn't exist.\\n- delete-note is destructive and asks the user to confirm; if a note was deleted by mistake, restore-note can bring it back.", + "instructions": "- Always format note titles as Markdown links using the note's URL.\\n- If you can't access the Apple Notes database, tell the user he needs to give Raycast the full disk access permission in the system settings. Be detailed about the steps.\\n- When calling get-note-content, update-note, append-to-note, delete-note, or move-note from search-notes results, use the note's id field and not UUID.\\n- Use update-note to replace a note's whole content, and append-to-note to add new content while keeping what's already there.\\n- Before calling move-note, call list-folders to find a valid folder name; move-note will fail if the folder doesn't exist. If list-folders shows the folder name in more than one account, pass that account as move-note's accountName, or it will fail with a duplicate-folder error.\\n- delete-note is destructive and asks the user to confirm; if a note was deleted by mistake, restore-note can bring it back.", "evals": [ { "input": "@apple-notes what's on my grocery list", diff --git a/extensions/apple-notes/src/tools/move-note.ts b/extensions/apple-notes/src/tools/move-note.ts index 18ac2457ca1..e6f34f06873 100644 --- a/extensions/apple-notes/src/tools/move-note.ts +++ b/extensions/apple-notes/src/tools/move-note.ts @@ -8,7 +8,10 @@ type Input = { noteId: string; /** The name of the destination folder. Use list-folders to find a valid folder name. */ folderName: string; - /** The name of the account that owns the destination folder, if known. */ + /** + * The name of the account that owns the destination folder. + * Required whenever list-folders shows more than one account with a folder of this name. + */ accountName?: string; }; From b429cda542c34f8524c91e0e349939eb61428d61 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Tue, 25 Aug 2026 17:21:44 -0400 Subject: [PATCH 08/12] Enhance note retrieval logic to support Unicode normalization and improve title matching accuracy. --- extensions/apple-notes/src/api/getNotes.ts | 57 ++++++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/extensions/apple-notes/src/api/getNotes.ts b/extensions/apple-notes/src/api/getNotes.ts index 2b4d3f2f631..c43a7441ae5 100644 --- a/extensions/apple-notes/src/api/getNotes.ts +++ b/extensions/apple-notes/src/api/getNotes.ts @@ -2,6 +2,39 @@ import { executeSQL } from "@raycast/utils"; import { escapeSQLString, getOpenNoteURL, NOTES_DB, Link, Backlink, Tag, NoteItem } from "../helpers"; +// SQLite's LOWER()/LIKE only fold ASCII case and never strip accents, so "cafe" wouldn't match +// "Café" in SQL. This is the authoritative, fully Unicode-aware check applied in JS; the SQL-side +// filter below only folds the common Latin accents, as a bound on how much data JS has to look at. +function normalizeForSearch(value: string): string { + return value + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .toLowerCase(); +} + +// Common Latin accented characters mapped to their base letter, used to build a SQL expression +// that approximates normalizeForSearch well enough to prefilter and bound the query in SQL. +const SQL_DIACRITIC_REPLACEMENTS: [string, string][] = [ + ["àáâãäå", "a"], + ["èéêë", "e"], + ["ìíîï", "i"], + ["òóôõö", "o"], + ["ùúûü", "u"], + ["ýÿ", "y"], + ["ñ", "n"], + ["ç", "c"], +]; + +function foldSqlColumn(column: string): string { + const withDiacriticsFolded = SQL_DIACRITIC_REPLACEMENTS.reduce((expr, [accentedChars, base]) => { + return [...accentedChars, ...accentedChars.toUpperCase()].reduce( + (inner, accentedChar) => `REPLACE(${inner}, '${accentedChar}', '${base}')`, + expr, + ); + }, column); + return `LOWER(${withDiacriticsFolded})`; +} + export async function getNotes( maxQueryResults: number, filterByTags: string[] = [], @@ -9,13 +42,13 @@ export async function getNotes( exactTitleMatch = false, ) { const trimmedSearchText = searchText?.trim(); - // Exact-title matches are filtered in SQL (before LIMIT) so a match can't be pushed out of the window by recency. + const foldedSearchText = trimmedSearchText ? escapeSQLString(normalizeForSearch(trimmedSearchText)) : ""; const searchFilter = trimmedSearchText ? exactTitleMatch - ? ` AND LOWER(TRIM(note.ztitle1)) = LOWER('${escapeSQLString(trimmedSearchText)}')` + ? ` AND ${foldSqlColumn("TRIM(note.ztitle1)")} = '${foldedSearchText}'` : ` AND ( - note.ztitle1 LIKE '%${escapeSQLString(trimmedSearchText)}%' OR - note.zsnippet LIKE '%${escapeSQLString(trimmedSearchText)}%' + ${foldSqlColumn("note.ztitle1")} LIKE '%${foldedSearchText}%' OR + ${foldSqlColumn("note.zsnippet")} LIKE '%${foldedSearchText}%' )` : ""; @@ -157,5 +190,21 @@ export async function getNotes( }); } + if (trimmedSearchText) { + if (exactTitleMatch) { + const normalizedQuery = trimmedSearchText.toLowerCase(); + notesWithAdditionalFields = notesWithAdditionalFields.filter( + (note) => note.title.trim().toLowerCase() === normalizedQuery, + ); + } else { + const normalizedQuery = normalizeForSearch(trimmedSearchText); + notesWithAdditionalFields = notesWithAdditionalFields.filter( + (note) => + normalizeForSearch(note.title).includes(normalizedQuery) || + normalizeForSearch(note.snippet).includes(normalizedQuery), + ); + } + } + return notesWithAdditionalFields; } From c2bef6648c2ab9ffcf2f22064f4f7198b89dbd54 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Thu, 27 Aug 2026 22:07:29 -0400 Subject: [PATCH 09/12] Refactor note filtering logic to simplify exact title match handling and improve search efficiency. --- extensions/apple-notes/src/api/getNotes.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/extensions/apple-notes/src/api/getNotes.ts b/extensions/apple-notes/src/api/getNotes.ts index c43a7441ae5..7c05e2cff02 100644 --- a/extensions/apple-notes/src/api/getNotes.ts +++ b/extensions/apple-notes/src/api/getNotes.ts @@ -190,20 +190,13 @@ export async function getNotes( }); } - if (trimmedSearchText) { - if (exactTitleMatch) { - const normalizedQuery = trimmedSearchText.toLowerCase(); - notesWithAdditionalFields = notesWithAdditionalFields.filter( - (note) => note.title.trim().toLowerCase() === normalizedQuery, - ); - } else { - const normalizedQuery = normalizeForSearch(trimmedSearchText); - notesWithAdditionalFields = notesWithAdditionalFields.filter( - (note) => - normalizeForSearch(note.title).includes(normalizedQuery) || - normalizeForSearch(note.snippet).includes(normalizedQuery), - ); - } + if (trimmedSearchText && !exactTitleMatch) { + const normalizedQuery = normalizeForSearch(trimmedSearchText); + notesWithAdditionalFields = notesWithAdditionalFields.filter( + (note) => + normalizeForSearch(note.title).includes(normalizedQuery) || + normalizeForSearch(note.snippet).includes(normalizedQuery), + ); } return notesWithAdditionalFields; From 311d5c8dfc6c214df03f64a1cfafc4356c188f8a Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Fri, 28 Aug 2026 12:07:14 -0400 Subject: [PATCH 10/12] Refactor note retrieval logic to enhance exact title matching with Unicode-aware checks and improve SQL query handling for non-ASCII characters. --- extensions/apple-notes/src/ai.ts | 3 +- extensions/apple-notes/src/api/getNotes.ts | 39 ++++++++++++++-------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/extensions/apple-notes/src/ai.ts b/extensions/apple-notes/src/ai.ts index c85167cfc85..38cac234ca7 100644 --- a/extensions/apple-notes/src/ai.ts +++ b/extensions/apple-notes/src/ai.ts @@ -15,7 +15,8 @@ export default async (props: LaunchProps<{ arguments: Arguments.Ai }>) => { if (noteQuery) { await showToast({ style: Toast.Style.Animated, title: "Looking for note" }); - // Exact title match is filtered in SQL, so it can't be missed regardless of how many notes exist. + // Exact title match is verified with Unicode-aware comparison, so it can't be missed + // regardless of how many notes exist or what script the title uses. const exactMatches = await getNotes(10, [], noteQuery, true); if (exactMatches.length === 1) { diff --git a/extensions/apple-notes/src/api/getNotes.ts b/extensions/apple-notes/src/api/getNotes.ts index 7c05e2cff02..a3b55349528 100644 --- a/extensions/apple-notes/src/api/getNotes.ts +++ b/extensions/apple-notes/src/api/getNotes.ts @@ -43,14 +43,21 @@ export async function getNotes( ) { const trimmedSearchText = searchText?.trim(); const foldedSearchText = trimmedSearchText ? escapeSQLString(normalizeForSearch(trimmedSearchText)) : ""; - const searchFilter = trimmedSearchText - ? exactTitleMatch - ? ` AND ${foldSqlColumn("TRIM(note.ztitle1)")} = '${foldedSearchText}'` - : ` AND ( - ${foldSqlColumn("note.ztitle1")} LIKE '%${foldedSearchText}%' OR - ${foldSqlColumn("note.zsnippet")} LIKE '%${foldedSearchText}%' - )` - : ""; + // SQLite's LOWER() only folds ASCII case, so a SQL-side fold can't be trusted to find exact + // matches that differ only by case in non-Latin scripts (e.g. "Привет" vs "ПРИВЕТ"). For those, + // skip the SQL filter and let the JS-side normalizeForSearch check below do the real matching. + const hasNonAsciiSearchText = trimmedSearchText + ? [...trimmedSearchText].some((char) => char.charCodeAt(0) > 127) + : false; + let searchFilter = ""; + if (trimmedSearchText && exactTitleMatch && !hasNonAsciiSearchText) { + searchFilter = ` AND ${foldSqlColumn("TRIM(note.ztitle1)")} = '${foldedSearchText}'`; + } else if (trimmedSearchText && !exactTitleMatch) { + searchFilter = ` AND ( + ${foldSqlColumn("note.ztitle1")} LIKE '%${foldedSearchText}%' OR + ${foldSqlColumn("note.zsnippet")} LIKE '%${foldedSearchText}%' + )`; + } const query = ` SELECT @@ -82,7 +89,7 @@ export async function getNotes( ${searchFilter} ORDER BY note.zmodificationdate1 DESC - LIMIT ${maxQueryResults} + LIMIT ${exactTitleMatch && hasNonAsciiSearchText ? -1 : maxQueryResults} `; const data = await executeSQL(NOTES_DB, query); @@ -190,13 +197,17 @@ export async function getNotes( }); } - if (trimmedSearchText && !exactTitleMatch) { + if (trimmedSearchText) { const normalizedQuery = normalizeForSearch(trimmedSearchText); - notesWithAdditionalFields = notesWithAdditionalFields.filter( - (note) => - normalizeForSearch(note.title).includes(normalizedQuery) || - normalizeForSearch(note.snippet).includes(normalizedQuery), + notesWithAdditionalFields = notesWithAdditionalFields.filter((note) => + exactTitleMatch + ? normalizeForSearch(note.title.trim()) === normalizedQuery + : normalizeForSearch(note.title).includes(normalizedQuery) || + normalizeForSearch(note.snippet).includes(normalizedQuery), ); + if (exactTitleMatch) { + notesWithAdditionalFields = notesWithAdditionalFields.slice(0, maxQueryResults); + } } return notesWithAdditionalFields; From 7ef81d1c6501dd3f4d1ace59cffc5895256a9ce4 Mon Sep 17 00:00:00 2001 From: "Noah K (BitForger)" Date: Fri, 28 Aug 2026 12:15:35 -0400 Subject: [PATCH 11/12] Refactor getNotes function to simplify SQL LIMIT handling and improve title matching logic. --- extensions/apple-notes/src/api/getNotes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/apple-notes/src/api/getNotes.ts b/extensions/apple-notes/src/api/getNotes.ts index a3b55349528..96b9ca8583f 100644 --- a/extensions/apple-notes/src/api/getNotes.ts +++ b/extensions/apple-notes/src/api/getNotes.ts @@ -89,7 +89,7 @@ export async function getNotes( ${searchFilter} ORDER BY note.zmodificationdate1 DESC - LIMIT ${exactTitleMatch && hasNonAsciiSearchText ? -1 : maxQueryResults} + LIMIT ${maxQueryResults} `; const data = await executeSQL(NOTES_DB, query); From cdb0d107c21f3a7034fe7436f182fe1ac411076d Mon Sep 17 00:00:00 2001 From: raycastbot Date: Tue, 1 Sep 2026 05:55:03 +0000 Subject: [PATCH 12/12] Update CHANGELOG.md --- extensions/apple-notes/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/apple-notes/CHANGELOG.md b/extensions/apple-notes/CHANGELOG.md index 312e57ef9c5..dbc3de0ae52 100644 --- a/extensions/apple-notes/CHANGELOG.md +++ b/extensions/apple-notes/CHANGELOG.md @@ -1,6 +1,6 @@ # Apple Notes Changelog -## [AI Extension Improvements] - {PR_MERGE_DATE} +## [AI Extension Improvements] - 2026-09-01 - Add `delete-note`, `restore-note`, `move-note`, `append-to-note`, and `list-folders` AI tools - Add tag filtering to the `search-notes` AI tool