apple-notes-mcp 1.2.18 → 1.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -7
- package/build/index.js +44 -6
- package/build/services/appleNotesManager.js +117 -28
- package/build/services/appleNotesManager.test.js +131 -0
- package/build/utils/checklistParser.js +259 -0
- package/build/utils/checklistParser.test.js +230 -0
- package/build/utils/protobuf.js +151 -0
- package/build/utils/protobuf.test.js +138 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ The AI assistant communicates with this server, which then uses AppleScript to i
|
|
|
25
25
|
If you're using [Claude Code](https://claude.com/product/claude-code) (in Terminal or VS Code), just ask Claude to install it:
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
Install the apple-notes-mcp MCP server so you can help me manage my Apple Notes
|
|
28
|
+
Install the sweetrb/apple-notes-mcp MCP server so you can help me manage my Apple Notes
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
Claude will handle the installation and configuration automatically.
|
|
@@ -45,7 +45,7 @@ This method also installs a **skill** that teaches Claude when and how to use Ap
|
|
|
45
45
|
|
|
46
46
|
**1. Install the server:**
|
|
47
47
|
```bash
|
|
48
|
-
npm install -g apple-notes-mcp
|
|
48
|
+
npm install -g github:sweetrb/apple-notes-mcp
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
**2. Add to Claude Desktop** (`~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
@@ -86,6 +86,7 @@ On first use, macOS will ask for permission to automate Notes.app. Click "OK" to
|
|
|
86
86
|
| **Folder Management** | Create, list, and delete folders |
|
|
87
87
|
| **Multi-Account** | Work with iCloud, Gmail, Exchange, or any configured account |
|
|
88
88
|
| **Batch Operations** | Delete or move multiple notes at once |
|
|
89
|
+
| **Checklist State** | Read checklist done/undone state directly from the Notes database |
|
|
89
90
|
| **Export** | Export all notes as JSON or get individual notes as Markdown |
|
|
90
91
|
| **Attachments** | List attachments in notes |
|
|
91
92
|
| **Sync Awareness** | Detect iCloud sync in progress, warn about incomplete results |
|
|
@@ -109,6 +110,7 @@ Creates a new note in Apple Notes.
|
|
|
109
110
|
| `title` | string | Yes | The title of the note (becomes first line) |
|
|
110
111
|
| `content` | string | Yes | The body content of the note |
|
|
111
112
|
| `tags` | string[] | No | Tags for organization (stored in metadata) |
|
|
113
|
+
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content is used as raw HTML for rich formatting |
|
|
112
114
|
|
|
113
115
|
**Example:**
|
|
114
116
|
```json
|
|
@@ -119,6 +121,15 @@ Creates a new note in Apple Notes.
|
|
|
119
121
|
}
|
|
120
122
|
```
|
|
121
123
|
|
|
124
|
+
**Example - HTML formatting:**
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"title": "Status Report",
|
|
128
|
+
"content": "<h1>Status Report</h1><h2>Summary</h2><p>All tasks <b>on track</b>.</p><ul><li>Feature A: complete</li><li>Feature B: in progress</li></ul>",
|
|
129
|
+
"format": "html"
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
122
133
|
**Returns:** Confirmation message with note title and ID. Save the ID for subsequent operations like `update-note`, `delete-note`, etc.
|
|
123
134
|
|
|
124
135
|
---
|
|
@@ -233,9 +244,10 @@ Updates an existing note's content and/or title.
|
|
|
233
244
|
|-----------|------|----------|-------------|
|
|
234
245
|
| `id` | string | No | Note ID (preferred - more reliable than title) |
|
|
235
246
|
| `title` | string | No | Current title of the note to update (use `id` instead when available) |
|
|
236
|
-
| `newTitle` | string | No | New title (if changing the title) |
|
|
247
|
+
| `newTitle` | string | No | New title (if changing the title; ignored when `format` is `"html"`) |
|
|
237
248
|
| `newContent` | string | Yes | New content for the note body |
|
|
238
249
|
| `account` | string | No | Account containing the note (defaults to iCloud, ignored if `id` is provided) |
|
|
250
|
+
| `format` | string | No | Content format: `"plaintext"` (default) or `"html"`. When `"html"`, content replaces the entire note body as raw HTML and `newTitle` is ignored (the first HTML element serves as the title) |
|
|
239
251
|
|
|
240
252
|
**Note:** Either `id` or `title` must be provided. Using `id` is recommended.
|
|
241
253
|
|
|
@@ -264,6 +276,15 @@ Updates an existing note's content and/or title.
|
|
|
264
276
|
}
|
|
265
277
|
```
|
|
266
278
|
|
|
279
|
+
**Example - Update with HTML formatting:**
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"id": "x-coredata://ABC123/ICNote/p456",
|
|
283
|
+
"newContent": "<h1>Updated Report</h1><p>New findings with <b>bold</b> emphasis.</p><pre><code>console.log('hello');</code></pre>",
|
|
284
|
+
"format": "html"
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
267
288
|
**Returns:** Confirmation message, or error if note not found.
|
|
268
289
|
|
|
269
290
|
---
|
|
@@ -476,7 +497,7 @@ Exports all notes as a JSON structure.
|
|
|
476
497
|
|
|
477
498
|
#### `get-note-markdown`
|
|
478
499
|
|
|
479
|
-
Gets a note's content as Markdown instead of HTML.
|
|
500
|
+
Gets a note's content as Markdown instead of HTML. If the note contains checklists and Full Disk Access is granted, checklist items are automatically annotated with `[x]` (done) or `[ ]` (undone).
|
|
480
501
|
|
|
481
502
|
| Parameter | Type | Required | Description |
|
|
482
503
|
|-----------|------|----------|-------------|
|
|
@@ -484,7 +505,35 @@ Gets a note's content as Markdown instead of HTML.
|
|
|
484
505
|
| `title` | string | No | Note title |
|
|
485
506
|
| `account` | string | No | Account containing the note |
|
|
486
507
|
|
|
487
|
-
**Returns:** Note content converted to Markdown format.
|
|
508
|
+
**Returns:** Note content converted to Markdown format. Checklist items include `[x]`/`[ ]` prefixes when database access is available.
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
#### `get-checklist-state`
|
|
513
|
+
|
|
514
|
+
Reads checklist done/undone state for a note. This bypasses the AppleScript limitation where `body of note` strips checklist state, by reading directly from the NoteStore SQLite database.
|
|
515
|
+
|
|
516
|
+
**Requires:** Full Disk Access for the MCP host process (see [Full Disk Access Setup](#full-disk-access-for-checklist-features)).
|
|
517
|
+
|
|
518
|
+
| Parameter | Type | Required | Description |
|
|
519
|
+
|-----------|------|----------|-------------|
|
|
520
|
+
| `id` | string | Yes | Note ID (use `search-notes` to find it first) |
|
|
521
|
+
|
|
522
|
+
**Example:**
|
|
523
|
+
```json
|
|
524
|
+
{
|
|
525
|
+
"id": "x-coredata://ABC123/ICNote/p456"
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
**Returns:** Checklist items with done/undone state and progress count:
|
|
530
|
+
```
|
|
531
|
+
Checklist for "Shopping List" (2/4 done):
|
|
532
|
+
[x] Buy milk
|
|
533
|
+
[x] Get bread
|
|
534
|
+
[ ] Pick up laundry
|
|
535
|
+
[ ] Call dentist
|
|
536
|
+
```
|
|
488
537
|
|
|
489
538
|
---
|
|
490
539
|
|
|
@@ -595,7 +644,7 @@ AI: [calls move-note with title="Old Meeting Notes", folder="Archive"]
|
|
|
595
644
|
### npm (Recommended)
|
|
596
645
|
|
|
597
646
|
```bash
|
|
598
|
-
npm install -g apple-notes-mcp
|
|
647
|
+
npm install -g github:sweetrb/apple-notes-mcp
|
|
599
648
|
```
|
|
600
649
|
|
|
601
650
|
### From Source
|
|
@@ -621,6 +670,30 @@ If installed from source, use this configuration:
|
|
|
621
670
|
|
|
622
671
|
---
|
|
623
672
|
|
|
673
|
+
## Full Disk Access for Checklist Features
|
|
674
|
+
|
|
675
|
+
The `get-checklist-state` tool and checklist annotations in `get-note-markdown` read directly from the Apple Notes SQLite database. This requires **Full Disk Access** for the process running the MCP server.
|
|
676
|
+
|
|
677
|
+
### How to Grant Full Disk Access
|
|
678
|
+
|
|
679
|
+
1. Open **System Settings** (or System Preferences on older macOS)
|
|
680
|
+
2. Go to **Privacy & Security > Full Disk Access**
|
|
681
|
+
3. Click the **+** button
|
|
682
|
+
4. Add the application that hosts the MCP server:
|
|
683
|
+
- **Claude Desktop**: Add `/Applications/Claude.app`
|
|
684
|
+
- **Terminal**: Add `/Applications/Utilities/Terminal.app`
|
|
685
|
+
- **VS Code**: Add `/Applications/Visual Studio Code.app`
|
|
686
|
+
- **iTerm**: Add `/Applications/iTerm.app`
|
|
687
|
+
5. Restart the application after granting access
|
|
688
|
+
|
|
689
|
+
### Without Full Disk Access
|
|
690
|
+
|
|
691
|
+
All other tools work normally without Full Disk Access. Only checklist state features are affected:
|
|
692
|
+
- `get-checklist-state` will return an error explaining that database access is needed
|
|
693
|
+
- `get-note-markdown` will return plain list items without `[x]`/`[ ]` annotations (graceful fallback)
|
|
694
|
+
|
|
695
|
+
---
|
|
696
|
+
|
|
624
697
|
## Security and Privacy
|
|
625
698
|
|
|
626
699
|
- **Local only** - All operations happen locally via AppleScript. No data is sent to external servers.
|
|
@@ -637,8 +710,9 @@ If installed from source, use this configuration:
|
|
|
637
710
|
| macOS only | Apple Notes and AppleScript are macOS-specific |
|
|
638
711
|
| No attachment content | Attachments can be listed but not downloaded via AppleScript |
|
|
639
712
|
| No pinned notes | Pin status is not exposed via AppleScript |
|
|
640
|
-
|
|
|
713
|
+
| Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
|
|
641
714
|
| Title matching | Most operations require exact title matches |
|
|
715
|
+
| Checklist state | Requires Full Disk Access to read done/undone state from the database |
|
|
642
716
|
|
|
643
717
|
### Backslash Escaping (Important for AI Agents)
|
|
644
718
|
|
|
@@ -717,3 +791,7 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
717
791
|
## Contributing
|
|
718
792
|
|
|
719
793
|
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
794
|
+
|
|
795
|
+
## Related Projects
|
|
796
|
+
|
|
797
|
+
- [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) - MCP server for Apple Mail
|
package/build/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
25
25
|
import { z } from "zod";
|
|
26
26
|
import { AppleNotesManager } from "./services/appleNotesManager.js";
|
|
27
27
|
import { getSyncStatus, withSyncAwarenessSync } from "./utils/syncDetection.js";
|
|
28
|
+
import { getChecklistItems, hasFullDiskAccess } from "./utils/checklistParser.js";
|
|
28
29
|
// Read version from package.json to keep it in sync
|
|
29
30
|
const require = createRequire(import.meta.url);
|
|
30
31
|
const { version } = require("../package.json");
|
|
@@ -112,9 +113,14 @@ const folderNameSchema = {
|
|
|
112
113
|
server.tool("create-note", {
|
|
113
114
|
title: z.string().min(1, "Title is required"),
|
|
114
115
|
content: z.string().min(1, "Content is required"),
|
|
116
|
+
format: z
|
|
117
|
+
.enum(["plaintext", "html"])
|
|
118
|
+
.optional()
|
|
119
|
+
.default("plaintext")
|
|
120
|
+
.describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
115
121
|
tags: z.array(z.string()).optional().describe("Tags for organization"),
|
|
116
|
-
}, withErrorHandling(({ title, content, tags = [] }) => {
|
|
117
|
-
const note = notesManager.createNote(title, content, tags);
|
|
122
|
+
}, withErrorHandling(({ title, content, format = "plaintext", tags = [] }) => {
|
|
123
|
+
const note = notesManager.createNote(title, content, tags, undefined, undefined, format);
|
|
118
124
|
if (!note) {
|
|
119
125
|
return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
|
|
120
126
|
}
|
|
@@ -244,11 +250,16 @@ server.tool("update-note", {
|
|
|
244
250
|
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
245
251
|
newTitle: z.string().optional().describe("New title for the note"),
|
|
246
252
|
newContent: z.string().min(1, "New content is required"),
|
|
253
|
+
format: z
|
|
254
|
+
.enum(["plaintext", "html"])
|
|
255
|
+
.optional()
|
|
256
|
+
.default("plaintext")
|
|
257
|
+
.describe("Content format: 'plaintext' (default) or 'html' for rich formatting"),
|
|
247
258
|
account: z
|
|
248
259
|
.string()
|
|
249
260
|
.optional()
|
|
250
261
|
.describe("Account containing the note (ignored if id is provided)"),
|
|
251
|
-
}, withErrorHandling(({ id, title, newTitle, newContent, account }) => {
|
|
262
|
+
}, withErrorHandling(({ id, title, newTitle, newContent, format = "plaintext", account }) => {
|
|
252
263
|
// Prefer ID-based update if provided
|
|
253
264
|
if (id) {
|
|
254
265
|
// Check for password protection first for better error message
|
|
@@ -259,7 +270,7 @@ server.tool("update-note", {
|
|
|
259
270
|
if (note.passwordProtected) {
|
|
260
271
|
return errorResponse(`Note "${note.title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`);
|
|
261
272
|
}
|
|
262
|
-
const success = notesManager.updateNoteById(id, newTitle, newContent);
|
|
273
|
+
const success = notesManager.updateNoteById(id, newTitle, newContent, format);
|
|
263
274
|
if (!success) {
|
|
264
275
|
return errorResponse(`Failed to update note "${note.title}"`);
|
|
265
276
|
}
|
|
@@ -282,7 +293,7 @@ server.tool("update-note", {
|
|
|
282
293
|
if (note.passwordProtected) {
|
|
283
294
|
return errorResponse(`Note "${title}" is password-protected and cannot be updated. Unlock it in Notes.app first.`);
|
|
284
295
|
}
|
|
285
|
-
const success = notesManager.updateNote(title, newTitle, newContent, account);
|
|
296
|
+
const success = notesManager.updateNote(title, newTitle, newContent, account, format);
|
|
286
297
|
if (!success) {
|
|
287
298
|
return errorResponse(`Failed to update note "${title}"`);
|
|
288
299
|
}
|
|
@@ -509,7 +520,12 @@ server.tool("health-check", {}, withErrorHandling(() => {
|
|
|
509
520
|
return ` ${icon} ${c.name}: ${c.message}`;
|
|
510
521
|
})
|
|
511
522
|
.join("\n");
|
|
512
|
-
|
|
523
|
+
// Check Full Disk Access for checklist features
|
|
524
|
+
const fdaAvailable = hasFullDiskAccess();
|
|
525
|
+
const fdaLine = fdaAvailable
|
|
526
|
+
? " ✓ full_disk_access: Granted (checklist features available)"
|
|
527
|
+
: " ⓘ full_disk_access: Not granted (optional — needed for get-checklist-state and checklist annotations in get-note-markdown). Grant in System Settings > Privacy & Security > Full Disk Access.";
|
|
528
|
+
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}`);
|
|
513
529
|
}, "Error running health check"));
|
|
514
530
|
// --- get-notes-stats ---
|
|
515
531
|
server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
@@ -662,6 +678,28 @@ server.tool("get-note-markdown", {
|
|
|
662
678
|
}
|
|
663
679
|
return successResponse(markdown);
|
|
664
680
|
}, "Error getting note as markdown"));
|
|
681
|
+
// --- get-checklist-state ---
|
|
682
|
+
server.tool("get-checklist-state", {
|
|
683
|
+
id: z.string().min(1, "Note ID is required. Use search-notes to find the note ID first."),
|
|
684
|
+
}, withErrorHandling(({ id }) => {
|
|
685
|
+
// Verify the note exists and is accessible
|
|
686
|
+
const note = notesManager.getNoteById(id);
|
|
687
|
+
if (!note) {
|
|
688
|
+
return errorResponse(`Note with ID "${id}" not found`);
|
|
689
|
+
}
|
|
690
|
+
if (note.passwordProtected) {
|
|
691
|
+
return errorResponse(`Note "${note.title}" is password-protected and cannot be read. Unlock it in Notes.app first.`);
|
|
692
|
+
}
|
|
693
|
+
const result = getChecklistItems(id);
|
|
694
|
+
if (!result.items) {
|
|
695
|
+
return errorResponse(result.message || "Failed to read checklist state.");
|
|
696
|
+
}
|
|
697
|
+
const summary = result.items
|
|
698
|
+
.map((item) => `${item.done ? "[x]" : "[ ]"} ${item.text}`)
|
|
699
|
+
.join("\n");
|
|
700
|
+
const checked = result.items.filter((i) => i.done).length;
|
|
701
|
+
return successResponse(`Checklist for "${note.title}" (${checked}/${result.items.length} done):\n${summary}`);
|
|
702
|
+
}, "Error reading checklist state"));
|
|
665
703
|
// =============================================================================
|
|
666
704
|
// Server Startup
|
|
667
705
|
// =============================================================================
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* @module services/appleNotesManager
|
|
16
16
|
*/
|
|
17
17
|
import { executeAppleScript } from "../utils/applescript.js";
|
|
18
|
+
import { getChecklistItems } from "../utils/checklistParser.js";
|
|
18
19
|
import TurndownService from "turndown";
|
|
19
20
|
// =============================================================================
|
|
20
21
|
// Text Processing Utilities
|
|
@@ -353,10 +354,11 @@ export class AppleNotesManager {
|
|
|
353
354
|
* to the account's default location.
|
|
354
355
|
*
|
|
355
356
|
* @param title - Display title for the note
|
|
356
|
-
* @param content - Body content (plain text
|
|
357
|
+
* @param content - Body content (plain text that will be HTML-escaped, or raw HTML when format is "html")
|
|
357
358
|
* @param tags - Optional tags (stored in returned object, not used by Notes.app)
|
|
358
359
|
* @param folder - Optional folder name to create the note in
|
|
359
360
|
* @param account - Account to use (defaults to iCloud)
|
|
361
|
+
* @param format - Content format: "plaintext" escapes and wraps in div tags (default), "html" uses content as-is
|
|
360
362
|
* @returns Created Note object with metadata, or null on failure
|
|
361
363
|
*
|
|
362
364
|
* @example
|
|
@@ -369,13 +371,17 @@ export class AppleNotesManager {
|
|
|
369
371
|
*
|
|
370
372
|
* // Create in a different account
|
|
371
373
|
* const gmail = manager.createNote("Draft", "...", [], undefined, "Gmail");
|
|
374
|
+
*
|
|
375
|
+
* // Create with HTML formatting
|
|
376
|
+
* const html = manager.createNote("Report", "<h1>Report</h1><p>Details here</p>",
|
|
377
|
+
* [], undefined, undefined, "html");
|
|
372
378
|
* ```
|
|
373
379
|
*/
|
|
374
|
-
createNote(title, content, tags = [], folder, account) {
|
|
380
|
+
createNote(title, content, tags = [], folder, account, format = "plaintext") {
|
|
375
381
|
const targetAccount = this.resolveAccount(account);
|
|
376
382
|
// Escape content for AppleScript embedding
|
|
377
383
|
const safeTitle = escapeForAppleScript(title);
|
|
378
|
-
const safeContent = escapeForAppleScript(content);
|
|
384
|
+
const safeContent = format === "html" ? escapeHtmlForAppleScript(content) : escapeForAppleScript(content);
|
|
379
385
|
// Build the AppleScript command
|
|
380
386
|
// Notes.app uses 'name' for the title and 'body' for content
|
|
381
387
|
// We capture the ID of the newly created note
|
|
@@ -690,25 +696,35 @@ export class AppleNotesManager {
|
|
|
690
696
|
* so updating content also allows title changes. If newTitle is
|
|
691
697
|
* not provided, the original title is preserved.
|
|
692
698
|
*
|
|
699
|
+
* When format is 'html', newTitle is ignored — the caller must include
|
|
700
|
+
* the title in the HTML content.
|
|
701
|
+
*
|
|
693
702
|
* Note: Password-protected notes will fail with an AppleScript error.
|
|
694
703
|
* Callers should check for password protection beforehand using
|
|
695
704
|
* getNoteDetails() or isNotePasswordProtected().
|
|
696
705
|
*
|
|
697
706
|
* @param title - Current title of the note to update
|
|
698
|
-
* @param newTitle - New title (optional, keeps existing if not provided)
|
|
707
|
+
* @param newTitle - New title (optional, keeps existing if not provided; ignored in html format)
|
|
699
708
|
* @param newContent - New content for the note body
|
|
700
709
|
* @param account - Account containing the note (defaults to iCloud)
|
|
710
|
+
* @param format - Content format: "plaintext" wraps in div tags (default), "html" uses content as-is
|
|
701
711
|
* @returns true if update succeeded, false otherwise
|
|
702
712
|
*/
|
|
703
|
-
updateNote(title, newTitle, newContent, account) {
|
|
713
|
+
updateNote(title, newTitle, newContent, account, format = "plaintext") {
|
|
704
714
|
const targetAccount = this.resolveAccount(account);
|
|
705
715
|
const safeCurrentTitle = escapeForAppleScript(title);
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
716
|
+
let fullBody;
|
|
717
|
+
if (format === "html") {
|
|
718
|
+
// HTML mode: content is the complete body, escaped only for AppleScript string
|
|
719
|
+
fullBody = escapeHtmlForAppleScript(newContent);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
// Plaintext mode: wrap title + content in <div> tags (existing behavior)
|
|
723
|
+
const effectiveTitle = newTitle || title;
|
|
724
|
+
const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
|
|
725
|
+
const safeContent = escapeForAppleScript(newContent);
|
|
726
|
+
fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
|
|
727
|
+
}
|
|
712
728
|
const updateCommand = `set body of note "${safeCurrentTitle}" to "${fullBody}"`;
|
|
713
729
|
const script = buildAccountScopedScript({ account: targetAccount }, updateCommand);
|
|
714
730
|
const result = executeAppleScript(script);
|
|
@@ -724,30 +740,41 @@ export class AppleNotesManager {
|
|
|
724
740
|
* This is more reliable than updateNote() because IDs are unique,
|
|
725
741
|
* while titles can be duplicated.
|
|
726
742
|
*
|
|
743
|
+
* When format is 'html', newTitle is ignored — the caller must include
|
|
744
|
+
* the title in the HTML content.
|
|
745
|
+
*
|
|
727
746
|
* Note: Password-protected notes will fail with an AppleScript error.
|
|
728
747
|
* Callers should check for password protection beforehand using
|
|
729
748
|
* getNoteById() or isNotePasswordProtectedById().
|
|
730
749
|
*
|
|
731
750
|
* @param id - CoreData URL identifier for the note
|
|
732
|
-
* @param newTitle - New title (optional, keeps existing if not provided)
|
|
751
|
+
* @param newTitle - New title (optional, keeps existing if not provided; ignored in html format)
|
|
733
752
|
* @param newContent - New content for the note body
|
|
753
|
+
* @param format - Content format: "plaintext" wraps in div tags (default), "html" uses content as-is
|
|
734
754
|
* @returns true if update succeeded, false otherwise
|
|
735
755
|
*/
|
|
736
|
-
updateNoteById(id, newTitle, newContent) {
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
756
|
+
updateNoteById(id, newTitle, newContent, format = "plaintext") {
|
|
757
|
+
let fullBody;
|
|
758
|
+
if (format === "html") {
|
|
759
|
+
// HTML mode: content is the complete body, escaped only for AppleScript string
|
|
760
|
+
fullBody = escapeHtmlForAppleScript(newContent);
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
// Plaintext mode: wrap title + content in <div> tags (existing behavior)
|
|
764
|
+
// Get the note to retrieve current title if newTitle not provided
|
|
765
|
+
let effectiveTitle = newTitle;
|
|
766
|
+
if (!effectiveTitle) {
|
|
767
|
+
const note = this.getNoteById(id);
|
|
768
|
+
if (!note) {
|
|
769
|
+
console.error(`Cannot update note: note with ID "${id}" not found`);
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
effectiveTitle = note.title;
|
|
744
773
|
}
|
|
745
|
-
|
|
774
|
+
const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
|
|
775
|
+
const safeContent = escapeForAppleScript(newContent);
|
|
776
|
+
fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
|
|
746
777
|
}
|
|
747
|
-
const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
|
|
748
|
-
const safeContent = escapeForAppleScript(newContent);
|
|
749
|
-
// Apple Notes uses HTML body; first <div> becomes the title
|
|
750
|
-
const fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
|
|
751
778
|
const updateCommand = `set body of note id "${id}" to "${fullBody}"`;
|
|
752
779
|
const script = buildAppLevelScript(updateCommand);
|
|
753
780
|
const result = executeAppleScript(script);
|
|
@@ -1585,9 +1612,52 @@ export class AppleNotesManager {
|
|
|
1585
1612
|
this.initTurndownService();
|
|
1586
1613
|
return this.turndownService.turndown(html).trim();
|
|
1587
1614
|
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Enriches markdown with checklist state from the NoteStore database.
|
|
1617
|
+
*
|
|
1618
|
+
* Apple Notes checklists appear as plain list items in the AppleScript HTML
|
|
1619
|
+
* output. This method reads the protobuf data to get done/undone state and
|
|
1620
|
+
* annotates matching list items with [x] or [ ] prefixes.
|
|
1621
|
+
*
|
|
1622
|
+
* Fails silently (returns original markdown) if the database is inaccessible
|
|
1623
|
+
* or the note has no checklists.
|
|
1624
|
+
*
|
|
1625
|
+
* @param markdown - The base markdown content
|
|
1626
|
+
* @param checklistItems - Checklist items with done state
|
|
1627
|
+
* @returns Markdown with checklist annotations
|
|
1628
|
+
*/
|
|
1629
|
+
enrichMarkdownWithChecklists(markdown, checklistItems) {
|
|
1630
|
+
if (checklistItems.length === 0)
|
|
1631
|
+
return markdown;
|
|
1632
|
+
// Build a map of checklist text → done state
|
|
1633
|
+
const checklistMap = new Map();
|
|
1634
|
+
for (const item of checklistItems) {
|
|
1635
|
+
checklistMap.set(item.text.trim(), item.done);
|
|
1636
|
+
}
|
|
1637
|
+
// Replace matching list items with checkbox syntax
|
|
1638
|
+
const lines = markdown.split("\n");
|
|
1639
|
+
const enriched = lines.map((line) => {
|
|
1640
|
+
// Match markdown list items: "- text" or "* text"
|
|
1641
|
+
const listMatch = line.match(/^(\s*[-*])\s+(.+)$/);
|
|
1642
|
+
if (!listMatch)
|
|
1643
|
+
return line;
|
|
1644
|
+
const [, prefix, text] = listMatch;
|
|
1645
|
+
const done = checklistMap.get(text.trim());
|
|
1646
|
+
if (done === undefined)
|
|
1647
|
+
return line;
|
|
1648
|
+
// Remove from map so duplicate text lines aren't all converted
|
|
1649
|
+
checklistMap.delete(text.trim());
|
|
1650
|
+
return `${prefix} ${done ? "[x]" : "[ ]"} ${text}`;
|
|
1651
|
+
});
|
|
1652
|
+
return enriched.join("\n");
|
|
1653
|
+
}
|
|
1588
1654
|
/**
|
|
1589
1655
|
* Gets note content as Markdown by title.
|
|
1590
1656
|
*
|
|
1657
|
+
* If the note contains checklists and the NoteStore database is accessible
|
|
1658
|
+
* (Full Disk Access required), checklist items will be annotated with
|
|
1659
|
+
* [x] (done) or [ ] (undone) prefixes.
|
|
1660
|
+
*
|
|
1591
1661
|
* @param title - Exact title of the note
|
|
1592
1662
|
* @param account - Account containing the note (defaults to iCloud)
|
|
1593
1663
|
* @returns Markdown content, or empty string if not found
|
|
@@ -1595,14 +1665,23 @@ export class AppleNotesManager {
|
|
|
1595
1665
|
* @example
|
|
1596
1666
|
* ```typescript
|
|
1597
1667
|
* const md = manager.getNoteMarkdown("Shopping List");
|
|
1598
|
-
* console.log(md); // "# Shopping List\n\n- Eggs\n- Milk"
|
|
1668
|
+
* console.log(md); // "# Shopping List\n\n- [x] Eggs\n- [ ] Milk"
|
|
1599
1669
|
* ```
|
|
1600
1670
|
*/
|
|
1601
1671
|
getNoteMarkdown(title, account) {
|
|
1602
1672
|
const html = this.getNoteContent(title, account);
|
|
1603
1673
|
if (!html)
|
|
1604
1674
|
return "";
|
|
1605
|
-
|
|
1675
|
+
let markdown = this.htmlToMarkdown(html);
|
|
1676
|
+
// Try to enrich with checklist state (requires note ID)
|
|
1677
|
+
const note = this.getNoteDetails(title, account);
|
|
1678
|
+
if (note?.id) {
|
|
1679
|
+
const result = getChecklistItems(note.id);
|
|
1680
|
+
if (result.items) {
|
|
1681
|
+
markdown = this.enrichMarkdownWithChecklists(markdown, result.items);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
return markdown;
|
|
1606
1685
|
}
|
|
1607
1686
|
/**
|
|
1608
1687
|
* Gets note content as Markdown by ID.
|
|
@@ -1610,6 +1689,10 @@ export class AppleNotesManager {
|
|
|
1610
1689
|
* This is more reliable than getNoteMarkdown() because IDs are unique
|
|
1611
1690
|
* across all accounts, while titles can be duplicated.
|
|
1612
1691
|
*
|
|
1692
|
+
* If the note contains checklists and the NoteStore database is accessible
|
|
1693
|
+
* (Full Disk Access required), checklist items will be annotated with
|
|
1694
|
+
* [x] (done) or [ ] (undone) prefixes.
|
|
1695
|
+
*
|
|
1613
1696
|
* @param id - CoreData URL identifier for the note
|
|
1614
1697
|
* @returns Markdown content, or empty string if not found
|
|
1615
1698
|
*/
|
|
@@ -1617,6 +1700,12 @@ export class AppleNotesManager {
|
|
|
1617
1700
|
const html = this.getNoteContentById(id);
|
|
1618
1701
|
if (!html)
|
|
1619
1702
|
return "";
|
|
1620
|
-
|
|
1703
|
+
let markdown = this.htmlToMarkdown(html);
|
|
1704
|
+
// Try to enrich with checklist state
|
|
1705
|
+
const result = getChecklistItems(id);
|
|
1706
|
+
if (result.items) {
|
|
1707
|
+
markdown = this.enrichMarkdownWithChecklists(markdown, result.items);
|
|
1708
|
+
}
|
|
1709
|
+
return markdown;
|
|
1621
1710
|
}
|
|
1622
1711
|
}
|