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 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
- | No rich formatting | Content is HTML; complex formatting may not render |
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
- return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}`);
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, will be HTML-escaped)
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
- // Determine the effective title (new or keep existing)
707
- const effectiveTitle = newTitle || title;
708
- const safeEffectiveTitle = escapeForAppleScript(effectiveTitle);
709
- const safeContent = escapeForAppleScript(newContent);
710
- // Apple Notes uses HTML body; first <div> becomes the title
711
- const fullBody = `<div>${safeEffectiveTitle}</div><div>${safeContent}</div>`;
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
- // Get the note to retrieve current title if newTitle not provided
738
- let effectiveTitle = newTitle;
739
- if (!effectiveTitle) {
740
- const note = this.getNoteById(id);
741
- if (!note) {
742
- console.error(`Cannot update note: note with ID "${id}" not found`);
743
- return false;
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
- effectiveTitle = note.title;
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
- return this.htmlToMarkdown(html);
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
- return this.htmlToMarkdown(html);
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
  }