Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions extensions/apple-notes/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
185 changes: 181 additions & 4 deletions extensions/apple-notes/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"andreaselia",
"fhf1121",
"charlesharries",
"janosorcsik"
"janosorcsik",
"bitforger"
],
"pastContributors": [
"thomaslombart"
Expand Down Expand Up @@ -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": [
{
Expand All @@ -85,6 +86,11 @@
"name": "instructions",
"type": "text",
"placeholder": "Additional instructions"
},
{
"name": "note",
"type": "text",
"placeholder": "Existing note (optional)"
}
]
},
Expand Down Expand Up @@ -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. 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",
Expand Down Expand Up @@ -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"
}
}
}
]
}
]
},
Expand Down Expand Up @@ -483,4 +660,4 @@
"platforms": [
"macOS"
]
}
}
52 changes: 46 additions & 6 deletions extensions/apple-notes/src/ai.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
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<ReturnType<typeof getNotes>>[number] | undefined;
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.
const exactMatches = await getNotes(10, [], noteQuery, true);

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",
});
return;
}
}

await showToast({
style: Toast.Style.Animated,
title: targetNote ? "Adding to note" : "Creating a note",
});

try {
const result = await AI.ask(
Expand All @@ -25,8 +58,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.",
});
}
};
72 changes: 72 additions & 0 deletions extensions/apple-notes/src/api/applescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,75 @@ 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, 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 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 (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(
`
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 },
);
}
Loading
Loading