Skip to content

Conversation

@vatsal22
Copy link
Contributor

@vatsal22 vatsal22 commented Feb 1, 2026

Add ability to edit Apple Notes while preserving the original created timestamp. Supports editing by note ID or title.

Add editNote() function in applescript.ts
Add 'notes edit' CLI command with --body, --title, --folder options
Add edit_note MCP tool for Claude Code integration
Add /notes:edit slash command documentation

vatsal22 and others added 3 commits February 1, 2026 13:36
Add note ID display to formatter output so users can easily find IDs
for use with the `read` command:
- formatNote(): show ID on folder line
- formatSearchResult(): show ID on folder line
- Update read command help text to clarify where to find IDs
Add ability to edit Apple Notes while preserving the original created
timestamp. Supports editing by note ID or title.

- Add editNote() function in applescript.ts
- Add 'notes edit' CLI command with --body, --title, --folder options
- Add edit_note MCP tool for Claude Code integration
- Add /notes:edit slash command documentation

Co-Authored-By: Claude Opus 4.5 <[email protected]>
feat: add edit command to update existing notes
@vatsal22 vatsal22 changed the title feat: Display note IDs in CLI output feat: Display note IDs in CLI output & add Edit command Feb 1, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds two features to improve the Apple Notes CLI tool: displaying note IDs in output and adding an edit command to modify existing notes while preserving their creation timestamps.

Changes:

  • Display note IDs in search and read command outputs for easy reference
  • Add notes edit CLI command with support for editing by ID or title
  • Add edit_note MCP tool for Claude integration

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/formatter.ts Updated formatNote() and formatSearchResult() to display note IDs alongside folder names
src/cli.ts Added 'edit' command with --body, --title, and --folder options; updated 'read' command help text
src/applescript.ts Implemented editNote() function to modify Apple Notes via AppleScript while preserving created timestamps
src/mcp.ts Added edit_note MCP tool with support for editing by ID or title
commands/edit.md Added documentation for the /notes:edit slash command with usage examples

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

const escapedTitle = escapeAppleScript(title);
// Apple Notes uses the first line of the body as the title, so we prepend the title
// as an HTML heading to preserve it when setting the body
const fullBody = `<h1>${title}</h1><br>${body}`;
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title is inserted directly into HTML without escaping, creating an XSS vulnerability. HTML special characters in the title should be escaped before inclusion in the HTML string.

Copilot uses AI. Check for mistakes.
};
}

const result = editNote({ title, body, folder });
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The editNote() call is not awaited, but it executes a synchronous operation via execSync. Consider making editNote async and awaiting it here for consistency with other async operations in this handler, or document why synchronous execution is intentional.

Suggested change
const result = editNote({ title, body, folder });
const result = await editNote({ title, body, folder });

Copilot uses AI. Check for mistakes.
Comment on lines +265 to +269
const result = editNote({
title,
body: options.body,
folder,
});
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The editNote() call is not awaited, but it executes a synchronous operation via execSync. Consider making editNote async and awaiting it here for consistency with other async operations in this action handler, or document why synchronous execution is intentional.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the pattern of other commands like create and delete

Copy link
Owner

@cardmagic cardmagic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from local TypeScript code review. Copilot already flagged the HTML injection issue — these are additional findings.

export function editNote(options: EditNoteOptions): EditNoteResult {
const { title, body, folder } = options;

const escapedTitle = escapeAppleScript(title);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

escapeAppleScript only handles \ and ". While single quotes are handled at the execSync level via the replace trick, the edit command takes a much larger body parameter than create/delete, expanding the injection surface.

Consider whether other AppleScript metacharacters (e.g. backslash+quote sequences, return, tab) in user-supplied body text could break out of the string literal. A more defensive approach would be to pass the body via stdin or a temp file rather than interpolating it into the script string.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I refactored to use input and escaped more characters

type: 'string',
description: 'Folder containing the note (for disambiguation when editing by title)',
},
},
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema declares required: ["body"] but the handler requires either id or title to locate the note. An LLM caller could invoke edit_note with only body and get a runtime error.

Consider either:

  • Adding a description like "Note: either id or title must also be provided" to the tool description
  • Or using a JSON Schema anyOf/oneOf to express the constraint

This would give LLM callers better guidance before invoking the tool.


try {
const result = execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, {
encoding: 'utf-8',
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The folder vs no-folder AppleScript branching duplicates the same pattern already in deleteNote. The two script blocks are nearly identical — the only difference is notes of targetFolder vs notes.

Not a blocker, but if you're open to it, a shared helper that builds the AppleScript with an optional folder scope would eliminate this duplication across editNote, deleteNote, and potentially future operations.

src/cli.ts Outdated

title = note.title;
folder = folder || note.folder;
} else if (options.title) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This project uses early returns to avoid nested conditionals. The if (id) / else if (options.title) / else block here could be flattened:

if (!id && !options.title) {
  console.error('Error: Either <id> or --title is required');
  process.exit(1);
}

if (id) {
  const noteId = parseInt(id, 10);
  if (isNaN(noteId)) {
    console.error('Invalid note ID');
    process.exit(1);
  }
  const note = await getNoteById(noteId);
  if (!note) {
    console.error('Note not found');
    process.exit(1);
  }
  title = note.title;
  folder = folder || note.folder;
}

if (options.title) {
  title = options.title;
}

Same applies to the MCP handler in src/mcp.ts.

@cardmagic
Copy link
Owner

Thanks for the contribution @vatsal22! The ID display in search/recent output is a great addition — it makes the read/edit workflow much more discoverable. Left a few inline comments on the edit command. Happy to merge the formatter changes right away if you want to split that out into its own PR.

@vatsal22 vatsal22 changed the title feat: Display note IDs in CLI output & add Edit command feat: Edit command Feb 9, 2026
@vatsal22
Copy link
Contributor Author

vatsal22 commented Feb 9, 2026

I moved the formatter changes into this PR: #21

Thanks for the review on the Edit feature. I'll work through the comments soon :)

@vatsal22 vatsal22 requested a review from cardmagic February 9, 2026 04:48
@cardmagic
Copy link
Owner

Thanks for the thorough follow-up @vatsal22! Every review comment has been addressed. The escapeHtml addition, the stdin-based executeAppleScript, the shared buildNoteOperationScript helper, the anyOf schema fix, and the early returns all look great.

Really appreciate that you went beyond the ask on a few of these... refactoring createNote, deleteNote, and listNoteFolders to use the new stdin approach and shared helper was a nice touch that improved the existing code too.

Merging this in. Thanks for the contribution!

@cardmagic cardmagic merged commit df0961b into cardmagic:main Feb 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants