From 20c2367e7689188263fd4bf866e50c8e9d9fb2e1 Mon Sep 17 00:00:00 2001 From: Vatsal Solanki Date: Sun, 1 Feb 2026 13:36:38 -0800 Subject: [PATCH 1/3] feat: display note IDs in CLI output 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 --- src/cli.ts | 2 +- src/formatter.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index 71943b8..2ce3f23 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,7 +72,7 @@ program program .command('read ') - .description('Read a note by ID') + .description('Read a note by ID (shown in search/recent output)') .action(async (id: string) => { try { const noteId = parseInt(id, 10); diff --git a/src/formatter.ts b/src/formatter.ts index 4bff8ce..137d5e5 100644 --- a/src/formatter.ts +++ b/src/formatter.ts @@ -23,8 +23,8 @@ export function formatNote(note: IndexedNote, showBody = false): string { const lockSuffix = note.isLocked ? chalk.red(' 🔒') : ''; lines.push(titlePrefix + chalk.bold.cyan(note.title || 'Untitled') + lockSuffix); - // Folder - lines.push(chalk.dim(`📁 ${note.folder}`)); + // Folder and ID + lines.push(chalk.dim(`📁 ${note.folder} | ID: ${note.id}`)); // Snippet if (note.snippet) { @@ -55,8 +55,8 @@ export function formatSearchResult( const highlightedTitle = highlightMatches(result.title || 'Untitled', query); lines.push(chalk.green('▶ ') + titlePrefix + chalk.bold.cyan(highlightedTitle) + lockSuffix); - // Folder - lines.push(chalk.dim(` 📁 ${result.folder}`)); + // Folder and ID + lines.push(chalk.dim(` 📁 ${result.folder} | ID: ${result.id}`)); // Highlighted snippet if (result.snippet) { From 152222ce8cad74a1a91f43b3fc702764d26e42f3 Mon Sep 17 00:00:00 2001 From: Vatsal Solanki Date: Sun, 1 Feb 2026 13:52:03 -0800 Subject: [PATCH 2/3] feat: add edit command to update existing notes 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 --- commands/edit.md | 33 +++++++++++++++++++++ src/applescript.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++ src/cli.ts | 54 +++++++++++++++++++++++++++++++++- src/mcp.ts | 62 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 commands/edit.md diff --git a/commands/edit.md b/commands/edit.md new file mode 100644 index 0000000..410e7e0 --- /dev/null +++ b/commands/edit.md @@ -0,0 +1,33 @@ +--- +description: Edit an existing Apple Note +allowed-tools: Bash(notes:*) +argument-hint: --body "new content" [--folder "Folder"] +--- + +# Edit Note + +Edit an existing note in Apple Notes. The created timestamp is preserved. + +## Instructions + +1. Check if the notes CLI is installed: +```bash +command -v notes || pnpm add -g @cardmagic/notes +``` + +2. Edit the note: +```bash +notes edit $ARGUMENTS +``` + +## Examples + +- `/notes:edit 123 --body "Updated content"` - Edit note by ID +- `/notes:edit --title "Meeting Notes" --body "New agenda"` - Edit by title +- `/notes:edit --title "Todo" --body "New tasks" --folder "Work"` - Edit with folder disambiguation + +## Workflow + +1. Find the note: `notes search "keyword"` or `notes recent` +2. Read current content: `notes read ` +3. Edit the note: `notes edit --body "new content"` diff --git a/src/applescript.ts b/src/applescript.ts index 65b199f..607f05a 100644 --- a/src/applescript.ts +++ b/src/applescript.ts @@ -17,6 +17,18 @@ export interface DeleteNoteResult { name: string; } +export interface EditNoteOptions { + title: string; + body: string; + folder?: string; +} + +export interface EditNoteResult { + success: boolean; + name: string; + folder: string; +} + function escapeAppleScript(str: string): string { return str.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); } @@ -110,6 +122,67 @@ export function deleteNote(title: string, folder?: string): DeleteNoteResult { } } +export function editNote(options: EditNoteOptions): EditNoteResult { + const { title, body, folder } = options; + + const escapedTitle = escapeAppleScript(title); + const escapedBody = escapeAppleScript(body); + + let script: string; + const targetFolder = folder || 'Notes'; + + if (folder) { + const escapedFolder = escapeAppleScript(folder); + script = ` + tell application "Notes" + set targetFolder to folder "${escapedFolder}" + set matchingNotes to notes of targetFolder whose name is "${escapedTitle}" + if (count of matchingNotes) is 0 then + error "Note not found" + end if + set targetNote to item 1 of matchingNotes + set body of targetNote to "${escapedBody}" + return name of targetNote + end tell + `; + } else { + script = ` + tell application "Notes" + set matchingNotes to notes whose name is "${escapedTitle}" + if (count of matchingNotes) is 0 then + error "Note not found" + end if + set targetNote to item 1 of matchingNotes + set body of targetNote to "${escapedBody}" + return name of targetNote + end tell + `; + } + + try { + const result = execSync(`osascript -e '${script.replace(/'/g, "'\"'\"'")}'`, { + encoding: 'utf-8', + timeout: 30000, + }); + + return { + success: true, + name: result.trim(), + folder: targetFolder, + }; + } catch (error) { + const message = (error as Error).message; + if (message.includes('Note not found')) { + const folderInfo = folder ? ` in folder "${folder}"` : ''; + throw new Error(`Note "${title}" not found${folderInfo}.`); + } + if (message.includes('get folder')) { + throw new Error(`Folder "${folder}" not found. Use 'notes folders' to list available folders.`); + } + throw new Error(`Failed to edit note: ${message}`); + } +} + export function listNoteFolders(): string[] { const script = ` tell application "Notes" diff --git a/src/cli.ts b/src/cli.ts index 2ce3f23..cb241cd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -23,7 +23,7 @@ import { formatIndexProgress, formatNote, } from './formatter.js'; -import { createNote, deleteNote } from './applescript.js'; +import { createNote, deleteNote, editNote } from './applescript.js'; const program = new Command(); @@ -224,6 +224,58 @@ program } }); +program + .command('edit [id]') + .description('Edit an existing note by ID or title') + .option('-t, --title ', 'Edit by title instead of ID') + .option('-b, --body ', 'New body content') + .option('-f, --folder ', 'Folder containing the note (for disambiguation)') + .action(async (id: string | undefined, options: { title?: string; body?: string; folder?: string }) => { + try { + if (!options.body) { + console.error('Error: --body is required'); + process.exit(1); + } + + let title: string; + let folder: string | undefined = options.folder; + + 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; + } else if (options.title) { + title = options.title; + } else { + console.error('Error: Either or --title is required'); + process.exit(1); + } + + const result = editNote({ + title, + body: options.body, + folder, + }); + console.log(`Updated note "${result.name}" in folder "${result.folder}"`); + } catch (error) { + console.error('Error:', (error as Error).message); + process.exit(1); + } finally { + closeConnections(); + } + }); + export function runCli(): void { program.parse(process.argv); } diff --git a/src/mcp.ts b/src/mcp.ts index a4a06fc..a784126 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -12,7 +12,7 @@ import { listFolders, getNoteStats, } from './searcher.js'; -import { createNote, deleteNote } from './applescript.js'; +import { createNote, deleteNote, editNote } from './applescript.js'; import type { IndexedNote, SearchResult } from './types.js'; function formatNoteForMcp(note: IndexedNote): string { @@ -204,6 +204,32 @@ export async function runMcpServer(): Promise { required: ['title'], }, }, + { + name: 'edit_note', + description: 'Edit an existing note in Apple Notes (preserves created timestamp)', + inputSchema: { + type: 'object', + properties: { + id: { + type: 'number', + description: 'Note ID to edit (from search/recent results)', + }, + title: { + type: 'string', + description: 'Edit by title instead of ID', + }, + body: { + type: 'string', + description: 'New body content for the note', + }, + folder: { + type: 'string', + description: 'Folder containing the note (for disambiguation when editing by title)', + }, + }, + required: ['body'], + }, + }, ], })); @@ -375,6 +401,40 @@ export async function runMcpServer(): Promise { }; } + case 'edit_note': { + const id = args?.id as number | undefined; + const titleArg = args?.title as string | undefined; + const body = args?.body as string; + let folder = args?.folder as string | undefined; + + let title: string; + + if (id !== undefined) { + const note = await getNoteById(id); + if (!note) { + return { + content: [{ type: 'text', text: `Note with ID ${id} not found.` }], + isError: true, + }; + } + title = note.title; + folder = folder || note.folder; + } else if (titleArg) { + title = titleArg; + } else { + return { + content: [{ type: 'text', text: 'Either id or title is required.' }], + isError: true, + }; + } + + const result = editNote({ title, body, folder }); + + return { + content: [{ type: 'text', text: `✅ Updated note "${result.name}" in folder "${result.folder}"` }], + }; + } + default: return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], From dd57c014fef58e5ae5afe2dcd19888e90a01038d Mon Sep 17 00:00:00 2001 From: Vatsal Solanki Date: Sun, 1 Feb 2026 14:11:55 -0800 Subject: [PATCH 3/3] preserve title --- src/applescript.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/applescript.ts b/src/applescript.ts index 607f05a..6cd1b19 100644 --- a/src/applescript.ts +++ b/src/applescript.ts @@ -126,7 +126,10 @@ export function editNote(options: EditNoteOptions): EditNoteResult { const { title, body, folder } = options; const escapedTitle = escapeAppleScript(title); - const escapedBody = escapeAppleScript(body); + // 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 = `

${title}


${body}`; + const escapedBody = escapeAppleScript(fullBody); let script: string; const targetFolder = folder || 'Notes';