Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
33 changes: 33 additions & 0 deletions commands/edit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
---
description: Edit an existing Apple Note
allowed-tools: Bash(notes:*)
argument-hint: <id> --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 <id>`
3. Edit the note: `notes edit <id> --body "new content"`
76 changes: 76 additions & 0 deletions src/applescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '\\"');
}
Expand Down Expand Up @@ -110,6 +122,70 @@ export function deleteNote(title: string, folder?: string): DeleteNoteResult {
}
}

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

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}`;
const escapedBody = escapeAppleScript(fullBody);

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"
Expand Down
56 changes: 54 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -72,7 +72,7 @@ program

program
.command('read <id>')
.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);
Expand Down Expand Up @@ -224,6 +224,58 @@ program
}
});

program
.command('edit [id]')
.description('Edit an existing note by ID or title')
.option('-t, --title <name>', 'Edit by title instead of ID')
.option('-b, --body <text>', 'New body content')
.option('-f, --folder <name>', '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 <id> 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);
}
8 changes: 4 additions & 4 deletions src/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
62 changes: 61 additions & 1 deletion src/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -204,6 +204,32 @@ export async function runMcpServer(): Promise<void> {
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'],
},
},
],
}));

Expand Down Expand Up @@ -375,6 +401,40 @@ export async function runMcpServer(): Promise<void> {
};
}

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}` }],
Expand Down