-
Notifications
You must be signed in to change notification settings - Fork 1
feat: Edit command #16
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
Conversation
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
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.
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 editCLI command with support for editing by ID or title - Add
edit_noteMCP 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.
src/applescript.ts
Outdated
| 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}`; |
Copilot
AI
Feb 5, 2026
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.
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.
| }; | ||
| } | ||
|
|
||
| const result = editNote({ title, body, folder }); |
Copilot
AI
Feb 5, 2026
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.
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.
| const result = editNote({ title, body, folder }); | |
| const result = await editNote({ title, body, folder }); |
| const result = editNote({ | ||
| title, | ||
| body: options.body, | ||
| folder, | ||
| }); |
Copilot
AI
Feb 5, 2026
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.
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.
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.
Following the pattern of other commands like create and delete
cardmagic
left a comment
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.
Review from local TypeScript code review. Copilot already flagged the HTML injection issue — these are additional findings.
src/applescript.ts
Outdated
| export function editNote(options: EditNoteOptions): EditNoteResult { | ||
| const { title, body, folder } = options; | ||
|
|
||
| const escapedTitle = escapeAppleScript(title); |
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.
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.
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.
I refactored to use input and escaped more characters
| type: 'string', | ||
| description: 'Folder containing the note (for disambiguation when editing by title)', | ||
| }, | ||
| }, |
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.
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/oneOfto express the constraint
This would give LLM callers better guidance before invoking the tool.
src/applescript.ts
Outdated
|
|
||
| try { | ||
| const result = execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, { | ||
| encoding: 'utf-8', |
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.
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) { |
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.
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.
|
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. |
|
I moved the formatter changes into this PR: #21 Thanks for the review on the Edit feature. I'll work through the comments soon :) |
|
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! |
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