apple-notes-mcp 2.1.0 → 2.1.2
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 +65 -7
- package/build/index.js +66 -27
- package/build/services/appleNotesManager.js +187 -28
- package/build/services/appleNotesManager.test.js +217 -9
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,6 +6,10 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that e
|
|
|
6
6
|
[](https://github.com/sweetrb/apple-notes-mcp/actions/workflows/ci.yml)
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="codex/assets/screenshot.png" alt="Apple Notes MCP — create, search, and organize Apple Notes from Codex, Claude, and other AI assistants" width="680">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
9
13
|
## What is This?
|
|
10
14
|
|
|
11
15
|
This server acts as a bridge between AI assistants and Apple Notes. Once configured, you can ask Claude (or any MCP-compatible AI) to:
|
|
@@ -52,6 +56,10 @@ codex plugin add apple-notes@apple-notes-mcp
|
|
|
52
56
|
|
|
53
57
|
The Codex plugin runs the published `apple-notes-mcp` server through `npx` and ships the same Apple Notes skill, so behavior matches the Claude Code plugin.
|
|
54
58
|
|
|
59
|
+
### Other Hosts (Hermes, Antigravity)
|
|
60
|
+
|
|
61
|
+
Plugin packaging for the Hermes and Antigravity hosts is also included (`.hermes-plugin/` and `.antigravity-plugin/`). Each registers the same `apple-notes` MCP server (launched via `npx -y apple-notes-mcp`) and bundles the Apple Notes skill, so behavior matches the Claude Code and Codex plugins. Install them through each host's plugin/marketplace mechanism pointed at this repository.
|
|
62
|
+
|
|
55
63
|
### Manual Installation
|
|
56
64
|
|
|
57
65
|
**1. Install the server:**
|
|
@@ -95,11 +103,12 @@ On first use, macOS will ask for permission to automate Notes.app. Click "OK" to
|
|
|
95
103
|
| **Delete Notes** | Remove notes (moves to Recently Deleted) |
|
|
96
104
|
| **Move Notes** | Organize notes into folders (supports nested paths) |
|
|
97
105
|
| **Folder Management** | Create, list, and delete folders with full hierarchical path support |
|
|
98
|
-
| **Multi-Account** | Work with iCloud, Gmail, Exchange, or any configured account |
|
|
106
|
+
| **Multi-Account** | Work with iCloud, Gmail, Exchange, or any configured account, including account IDs and default folders |
|
|
99
107
|
| **Batch Operations** | Delete or move multiple notes at once |
|
|
100
108
|
| **Checklist State** | Read checklist done/undone state directly from the Notes database (requires Full Disk Access) |
|
|
101
109
|
| **Export** | Export all notes as JSON or get individual notes as Markdown |
|
|
102
110
|
| **Attachments** | List attachments, save them to disk, or fetch their bytes as base64 |
|
|
111
|
+
| **Notes.app UI State** | Reveal a note in Notes.app or read the current Notes.app selection |
|
|
103
112
|
| **Sync Awareness** | Detect iCloud sync in progress, warn about incomplete results |
|
|
104
113
|
| **Collaboration** | Detect shared notes, warn before modifying |
|
|
105
114
|
| **Diagnostics** | `health-check` plus a richer `doctor` (reachability, automation permission, accounts, Full Disk Access), sync status, and statistics |
|
|
@@ -294,6 +303,19 @@ Retrieves a note using its unique CoreData identifier.
|
|
|
294
303
|
|
|
295
304
|
---
|
|
296
305
|
|
|
306
|
+
#### `show-note`
|
|
307
|
+
|
|
308
|
+
Reveals a note in Notes.app using its unique CoreData identifier.
|
|
309
|
+
|
|
310
|
+
| Parameter | Type | Required | Description |
|
|
311
|
+
|-----------|------|----------|-------------|
|
|
312
|
+
| `id` | string | Yes | The CoreData URL identifier (e.g., `x-coredata://...`) |
|
|
313
|
+
| `separately` | boolean | No | Open in a separate note window when supported by Notes.app |
|
|
314
|
+
|
|
315
|
+
**Returns:** Confirmation that Notes.app accepted the show command.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
297
319
|
#### `update-note`
|
|
298
320
|
|
|
299
321
|
Updates an existing note's content and/or title.
|
|
@@ -345,6 +367,8 @@ Updates an existing note's content and/or title.
|
|
|
345
367
|
|
|
346
368
|
**Returns:** Confirmation message, or error if note not found.
|
|
347
369
|
|
|
370
|
+
**Note:** `newContent` **replaces the entire note body** — it is not appended. To preserve existing content, read it first (e.g. with `get-note-content`) and include it in `newContent`.
|
|
371
|
+
|
|
348
372
|
---
|
|
349
373
|
|
|
350
374
|
#### `delete-note`
|
|
@@ -375,6 +399,8 @@ Deletes a note (moves to Recently Deleted in Notes.app).
|
|
|
375
399
|
|
|
376
400
|
**Returns:** Confirmation message, or error if note not found.
|
|
377
401
|
|
|
402
|
+
**⚠️ Safety:** Irreversible from the agent's side — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact id(s) being deleted.
|
|
403
|
+
|
|
378
404
|
---
|
|
379
405
|
|
|
380
406
|
#### `move-note`
|
|
@@ -447,6 +473,16 @@ Lists all notes, optionally filtered by folder, date, and limit.
|
|
|
447
473
|
|
|
448
474
|
---
|
|
449
475
|
|
|
476
|
+
#### `get-selected-notes`
|
|
477
|
+
|
|
478
|
+
Reads the currently selected note(s) from the Notes.app UI.
|
|
479
|
+
|
|
480
|
+
**Parameters:** None
|
|
481
|
+
|
|
482
|
+
**Returns:** Selected note metadata, including IDs for follow-up operations. Returns an empty list when Notes.app has no selected note.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
450
486
|
### Folder Operations
|
|
451
487
|
|
|
452
488
|
#### `list-folders`
|
|
@@ -462,7 +498,7 @@ Lists all folders in an account with full hierarchical paths.
|
|
|
462
498
|
{}
|
|
463
499
|
```
|
|
464
500
|
|
|
465
|
-
**Returns:** List of
|
|
501
|
+
**Returns:** List of folders with IDs, paths, account names, and shared state. Nested folders are shown as full paths (e.g., `Work/Clients/Omnia`). Duplicate folder names are disambiguated by their full path. Literal slashes in folder names are escaped as `\/` (e.g., `Spain\/Portugal 2023`).
|
|
466
502
|
|
|
467
503
|
---
|
|
468
504
|
|
|
@@ -504,6 +540,8 @@ Deletes a folder.
|
|
|
504
540
|
|
|
505
541
|
**Returns:** Confirmation message, or error if folder not found or not empty.
|
|
506
542
|
|
|
543
|
+
**⚠️ Safety:** Irreversible — requires explicit user confirmation before calling. Prefer `list-folders` first to confirm the exact folder path being deleted.
|
|
544
|
+
|
|
507
545
|
---
|
|
508
546
|
|
|
509
547
|
### Account Operations
|
|
@@ -519,7 +557,17 @@ Lists all configured Notes accounts.
|
|
|
519
557
|
{}
|
|
520
558
|
```
|
|
521
559
|
|
|
522
|
-
**Returns:** List of
|
|
560
|
+
**Returns:** List of accounts with names, IDs, upgraded state, and default folder metadata.
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
#### `get-default-location`
|
|
565
|
+
|
|
566
|
+
Returns the default account and folder Notes.app uses for newly created notes.
|
|
567
|
+
|
|
568
|
+
**Parameters:** None
|
|
569
|
+
|
|
570
|
+
**Returns:** Default account and folder metadata, including IDs and shared state.
|
|
523
571
|
|
|
524
572
|
---
|
|
525
573
|
|
|
@@ -535,6 +583,8 @@ Deletes multiple notes at once by ID.
|
|
|
535
583
|
|
|
536
584
|
**Returns:** Summary of successes and failures.
|
|
537
585
|
|
|
586
|
+
**⚠️ Safety:** Irreversible — requires explicit user confirmation before calling. Prefer `search-notes` / `list-notes` first to confirm the exact ids being deleted.
|
|
587
|
+
|
|
538
588
|
---
|
|
539
589
|
|
|
540
590
|
#### `batch-move-notes`
|
|
@@ -615,7 +665,7 @@ Lists attachments in a note.
|
|
|
615
665
|
| `title` | string | No | Note title |
|
|
616
666
|
| `account` | string | No | Account containing the note |
|
|
617
667
|
|
|
618
|
-
**Returns:** List of attachments with names and
|
|
668
|
+
**Returns:** List of attachments with IDs, names, content identifiers, URLs when available, created/modified dates, and shared state.
|
|
619
669
|
|
|
620
670
|
---
|
|
621
671
|
|
|
@@ -676,6 +726,8 @@ Gets comprehensive statistics about your notes.
|
|
|
676
726
|
|
|
677
727
|
**Returns:** Total counts, per-account breakdown, folder statistics, and recently modified counts.
|
|
678
728
|
|
|
729
|
+
The `structuredContent` also includes a `coverage` object — `{ complete, scanned, covered, warnings[] }`. If `complete` is `false`, one or more accounts (or the recent-activity scan) could not be read and the counts reflect only the scopes that succeeded; the text output adds a "⚠️ Partial results" line. This lets you tell a genuinely empty library apart from a partial failure.
|
|
730
|
+
|
|
679
731
|
---
|
|
680
732
|
|
|
681
733
|
#### `get-sync-status`
|
|
@@ -1005,6 +1057,12 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
|
1005
1057
|
|
|
1006
1058
|
## Related Projects
|
|
1007
1059
|
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
- [apple-
|
|
1060
|
+
Part of a family of macOS MCP servers:
|
|
1061
|
+
|
|
1062
|
+
- [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) — MCP server for Apple Mail (read, search, send, and organize email)
|
|
1063
|
+
- [apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp) — MCP server for Apple Numbers (read and write .numbers spreadsheets)
|
|
1064
|
+
- [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos (query metadata and export originals)
|
|
1065
|
+
|
|
1066
|
+
## Recurring macOS permission prompts
|
|
1067
|
+
|
|
1068
|
+
If macOS keeps re-prompting for Full Disk Access or Automation for `node` (often after a `brew upgrade`), see [docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md](docs/NODE-RUNTIME-AND-TCC-PERMISSIONS.md) — the fix is to run this server under the official, Developer-ID-signed Node so the grant survives Node updates.
|
package/build/index.js
CHANGED
|
@@ -108,7 +108,7 @@ const folderNameSchema = {
|
|
|
108
108
|
// Note Tools
|
|
109
109
|
// =============================================================================
|
|
110
110
|
// --- create-note ---
|
|
111
|
-
server.tool("create-note", {
|
|
111
|
+
server.tool("create-note", "Use when: the user wants to create a brand-new Apple Note.\nReturns: the new note's title and id — reuse the id for follow-up reads/edits.\nDo not use when: editing an existing note (use update-note).\nNote: the title is prepended as an <h1>; true Apple Notes checklists cannot be created via AppleScript (see the content field).", {
|
|
112
112
|
title: z.string().min(1, "Title is required"),
|
|
113
113
|
content: z
|
|
114
114
|
.string()
|
|
@@ -134,7 +134,7 @@ server.tool("create-note", {
|
|
|
134
134
|
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`);
|
|
135
135
|
}, "Error creating note"));
|
|
136
136
|
// --- search-notes ---
|
|
137
|
-
server.tool("search-notes", {
|
|
137
|
+
server.tool("search-notes", "Use when: finding notes by a keyword in the title (or body with searchContent=true) and you need their ids.\nReturns: matching notes with title, folder, and id.\nDo not use when: you already have a note id (use get-note-content) or want every note (use list-notes).\nPrefer this first to obtain ids for subsequent read/update/delete/move calls.", {
|
|
138
138
|
query: z.string().min(1, "Search query is required"),
|
|
139
139
|
searchContent: z.boolean().optional().describe("Search note content instead of titles"),
|
|
140
140
|
account: z.string().optional().describe("Account to search in"),
|
|
@@ -179,7 +179,7 @@ server.tool("search-notes", {
|
|
|
179
179
|
return successResponse(`Found ${notes.length} notes (searched ${searchType}${folderInfo}${dateInfo}${limitInfo}):\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
180
180
|
}, "Error searching notes"));
|
|
181
181
|
// --- get-note-content ---
|
|
182
|
-
server.tool("get-note-content", {
|
|
182
|
+
server.tool("get-note-content", "Use when: reading the full body text of one known note, by id (preferred) or title.\nReturns: the note's content plus parsed hashtags.\nDo not use when: you only need metadata (get-note-details) or Markdown with checklist state (get-note-markdown).\nNote: password-protected notes must be unlocked in Notes.app first.", {
|
|
183
183
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
184
184
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
185
185
|
account: z
|
|
@@ -224,7 +224,7 @@ server.tool("get-note-content", {
|
|
|
224
224
|
return successResponse(content, { title, content, hashtags });
|
|
225
225
|
}, "Error retrieving note content"));
|
|
226
226
|
// --- get-note-by-id ---
|
|
227
|
-
server.tool("get-note-by-id", {
|
|
227
|
+
server.tool("get-note-by-id", "Use when: you have a note id and need its metadata only.\nReturns: id, title, created, modified, shared, passwordProtected.\nDo not use when: you need the body text (get-note-content) or only have a title (get-note-details).", {
|
|
228
228
|
id: z.string().min(1, "Note ID is required"),
|
|
229
229
|
}, withErrorHandling(({ id }) => {
|
|
230
230
|
const note = notesManager.getNoteById(id);
|
|
@@ -243,7 +243,7 @@ server.tool("get-note-by-id", {
|
|
|
243
243
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
244
244
|
}, "Error retrieving note"));
|
|
245
245
|
// --- get-note-details ---
|
|
246
|
-
server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, account }) => {
|
|
246
|
+
server.tool("get-note-details", "Use when: you have a note title (not an id) and need its metadata.\nReturns: id, title, created, modified, shared, passwordProtected, account.\nDo not use when: you have an id (get-note-by-id) or need the body text (get-note-content).\nUse the returned id for reliable follow-up operations.", noteTitleSchema, withErrorHandling(({ title, account }) => {
|
|
247
247
|
const note = notesManager.getNoteDetails(title, account);
|
|
248
248
|
if (!note) {
|
|
249
249
|
return errorResponse(`Note "${title}" not found`);
|
|
@@ -260,8 +260,22 @@ server.tool("get-note-details", noteTitleSchema, withErrorHandling(({ title, acc
|
|
|
260
260
|
};
|
|
261
261
|
return successResponse(JSON.stringify(metadata, null, 2), metadata);
|
|
262
262
|
}, "Error retrieving note details"));
|
|
263
|
+
// --- show-note ---
|
|
264
|
+
server.tool("show-note", "Use when: the user wants to reveal a known note in Notes.app by id.\nReturns: confirmation that Notes.app accepted the show command.\nDo not use when: you only need note content (get-note-content) or metadata (get-note-by-id).\nNote: this opens or focuses the Notes UI.", {
|
|
265
|
+
id: z.string().min(1, "Note ID is required"),
|
|
266
|
+
separately: z
|
|
267
|
+
.boolean()
|
|
268
|
+
.optional()
|
|
269
|
+
.describe("Open in a separate note window when supported by Notes.app"),
|
|
270
|
+
}, withErrorHandling(({ id, separately = false }) => {
|
|
271
|
+
const success = notesManager.showNoteById(id, separately);
|
|
272
|
+
if (!success) {
|
|
273
|
+
return errorResponse(`Failed to show note with ID "${id}"`);
|
|
274
|
+
}
|
|
275
|
+
return successResponse(`Shown note with ID "${id}" in Notes.app`, { id, separately });
|
|
276
|
+
}, "Error showing note"));
|
|
263
277
|
// --- update-note ---
|
|
264
|
-
server.tool("update-note", {
|
|
278
|
+
server.tool("update-note", "Use when: changing the title and/or replacing the body of an existing note, by id (preferred) or title.\nReturns: confirmation; warns when the note is shared.\nDo not use when: creating a new note (create-note).\nSafety: newContent REPLACES the entire body — it does not append. Read the note first if you need to preserve existing text. Edits to shared notes are immediately visible to all collaborators.", {
|
|
265
279
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
266
280
|
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
267
281
|
newTitle: z.string().optional().describe("New title for the note"),
|
|
@@ -326,7 +340,7 @@ server.tool("update-note", {
|
|
|
326
340
|
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`);
|
|
327
341
|
}, "Error updating note"));
|
|
328
342
|
// --- delete-note ---
|
|
329
|
-
server.tool("delete-note", {
|
|
343
|
+
server.tool("delete-note", "Use when: permanently deleting a single note, by id (preferred) or title.\nReturns: confirmation; warns when the note was shared.\nDo not use when: deleting many notes (batch-delete-notes) or just relocating one (move-note).\nSafety: requires explicit user confirmation before deleting. Prefer search-notes/list-notes first to show the affected note id and title. Deleting a shared note removes collaborator access.", {
|
|
330
344
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
331
345
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
332
346
|
account: z
|
|
@@ -371,7 +385,7 @@ server.tool("delete-note", {
|
|
|
371
385
|
return successResponse(`Note deleted: "${title}"${sharedWarning}`);
|
|
372
386
|
}, "Error deleting note"));
|
|
373
387
|
// --- move-note ---
|
|
374
|
-
server.tool("move-note", {
|
|
388
|
+
server.tool("move-note", "Use when: moving one note to a different folder, by id (preferred) or title.\nReturns: confirmation of the note and destination folder.\nDo not use when: moving many notes (batch-move-notes).\nNote: implemented as copy-then-delete; the destination folder must already exist (create-folder).", {
|
|
375
389
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
376
390
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
377
391
|
folder: z.string().min(1, "Destination folder is required"),
|
|
@@ -406,7 +420,7 @@ server.tool("move-note", {
|
|
|
406
420
|
return successResponse(`Note moved: "${title}" -> "${folder}"`);
|
|
407
421
|
}, "Error moving note"));
|
|
408
422
|
// --- list-notes ---
|
|
409
|
-
server.tool("list-notes", {
|
|
423
|
+
server.tool("list-notes", "Use when: enumerating notes in an account or folder; supports modifiedSince and limit for large collections.\nReturns: note titles only (no content or ids).\nDo not use when: you need content (get-note-content) or ids for follow-up edits (use search-notes).\nNote: warns if iCloud sync is active and results may be partial.", {
|
|
410
424
|
account: z.string().optional().describe("Account to list notes from"),
|
|
411
425
|
folder: z.string().optional().describe("Filter to specific folder"),
|
|
412
426
|
modifiedSince: z
|
|
@@ -440,11 +454,23 @@ server.tool("list-notes", {
|
|
|
440
454
|
const noteList = notes.map((t) => ` - ${t}`).join("\n");
|
|
441
455
|
return successResponse(`Found ${notes.length} notes${location}${acct}${dateInfo}${limitInfo}:\n${noteList}${syncNote}`, { notes, count: notes.length });
|
|
442
456
|
}, "Error listing notes"));
|
|
457
|
+
// --- get-selected-notes ---
|
|
458
|
+
server.tool("get-selected-notes", "Use when: the user asks what note(s) are currently selected in Notes.app.\nReturns: selected note metadata with ids for follow-up operations.\nDo not use when: searching all notes (search-notes) or listing a folder (list-notes).\nNote: reads Notes.app UI selection; it may be empty if Notes is closed or no note is selected.", {}, withErrorHandling(() => {
|
|
459
|
+
const notes = notesManager.getSelectedNotes();
|
|
460
|
+
if (notes.length === 0) {
|
|
461
|
+
return successResponse("No notes are currently selected in Notes.app", {
|
|
462
|
+
notes: [],
|
|
463
|
+
count: 0,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
const noteList = notes.map((n) => ` - ${n.title} [id: ${n.id}]`).join("\n");
|
|
467
|
+
return successResponse(`Selected note(s):\n${noteList}`, { notes, count: notes.length });
|
|
468
|
+
}, "Error getting selected notes"));
|
|
443
469
|
// =============================================================================
|
|
444
470
|
// Folder Tools
|
|
445
471
|
// =============================================================================
|
|
446
472
|
// --- list-folders ---
|
|
447
|
-
server.tool("list-folders", {
|
|
473
|
+
server.tool("list-folders", "Use when: listing all folders, with full nested paths, for an account.\nReturns: folder names/paths.\nDo not use when: listing notes (list-notes).\nNote: warns if iCloud sync is active.", {
|
|
448
474
|
account: z.string().optional().describe("Account to list folders from"),
|
|
449
475
|
}, withErrorHandling(({ account }) => {
|
|
450
476
|
// Use sync-aware wrapper for this read operation
|
|
@@ -469,7 +495,7 @@ server.tool("list-folders", {
|
|
|
469
495
|
});
|
|
470
496
|
}, "Error listing folders"));
|
|
471
497
|
// --- create-folder ---
|
|
472
|
-
server.tool("create-folder", {
|
|
498
|
+
server.tool("create-folder", "Use when: creating a folder, including nested paths like 'Work/Clients' (intermediate folders are created, existing ones skipped).\nReturns: confirmation.\nDo not use when: creating a note (create-note).", {
|
|
473
499
|
name: z
|
|
474
500
|
.string()
|
|
475
501
|
.min(1, "Folder name is required")
|
|
@@ -483,7 +509,7 @@ server.tool("create-folder", {
|
|
|
483
509
|
return successResponse(`Folder created: "${folder.name}"`);
|
|
484
510
|
}, "Error creating folder"));
|
|
485
511
|
// --- delete-folder ---
|
|
486
|
-
server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, account }) => {
|
|
512
|
+
server.tool("delete-folder", "Use when: deleting an existing folder by name or nested path.\nReturns: confirmation.\nDo not use when: deleting a note (delete-note).\nSafety: requires explicit user confirmation. Deletion fails if the folder still contains notes — list or move those notes first.", folderNameSchema, withErrorHandling(({ name, account }) => {
|
|
487
513
|
const success = notesManager.deleteFolder(name, account);
|
|
488
514
|
if (!success) {
|
|
489
515
|
return errorResponse(`Failed to delete folder "${name}". Folder may not exist or may contain notes.`);
|
|
@@ -494,22 +520,35 @@ server.tool("delete-folder", folderNameSchema, withErrorHandling(({ name, accoun
|
|
|
494
520
|
// Account Tools
|
|
495
521
|
// =============================================================================
|
|
496
522
|
// --- list-accounts ---
|
|
497
|
-
server.tool("list-accounts", {}, withErrorHandling(() => {
|
|
523
|
+
server.tool("list-accounts", "Use when: discovering which Notes accounts exist (iCloud, Gmail, Exchange, etc.) before targeting one.\nReturns: account names.\nDo not use when: you already know the account, or are working by note id (ids are account-independent).", {}, withErrorHandling(() => {
|
|
498
524
|
const accounts = notesManager.listAccounts();
|
|
499
525
|
if (accounts.length === 0) {
|
|
500
526
|
return successResponse("No Notes accounts found", { accounts: [], count: 0 });
|
|
501
527
|
}
|
|
502
|
-
const accountList = accounts
|
|
528
|
+
const accountList = accounts
|
|
529
|
+
.map((a) => {
|
|
530
|
+
const defaultFolder = a.defaultFolder ? ` (default folder: ${a.defaultFolder})` : "";
|
|
531
|
+
const upgraded = a.upgraded === undefined ? "" : `, upgraded: ${a.upgraded ? "yes" : "no"}`;
|
|
532
|
+
return ` - ${a.name}${defaultFolder}${upgraded}`;
|
|
533
|
+
})
|
|
534
|
+
.join("\n");
|
|
503
535
|
return successResponse(`Found ${accounts.length} accounts:\n${accountList}`, {
|
|
504
536
|
accounts,
|
|
505
537
|
count: accounts.length,
|
|
506
538
|
});
|
|
507
539
|
}, "Error listing accounts"));
|
|
540
|
+
// --- get-default-location ---
|
|
541
|
+
server.tool("get-default-location", "Use when: discovering where Notes.app will create new notes by default.\nReturns: default account and default folder metadata.\nDo not use when: you already have an explicit account/folder target.", {}, withErrorHandling(() => {
|
|
542
|
+
const location = notesManager.getDefaultLocation();
|
|
543
|
+
const message = `Default account: ${location.account.name} [id: ${location.account.id}]\n` +
|
|
544
|
+
`Default folder: ${location.folder.name} [id: ${location.folder.id}]`;
|
|
545
|
+
return successResponse(message, { ...location });
|
|
546
|
+
}, "Error getting default Notes location"));
|
|
508
547
|
// =============================================================================
|
|
509
548
|
// Collaboration Tools
|
|
510
549
|
// =============================================================================
|
|
511
550
|
// --- list-shared-notes ---
|
|
512
|
-
server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
551
|
+
server.tool("list-shared-notes", "Use when: finding notes shared with collaborators.\nReturns: shared notes with title, account, and id.\nDo not use when: searching all notes (search-notes).\nNote: edits or deletes to these notes affect all collaborators.", {}, withErrorHandling(() => {
|
|
513
552
|
const sharedNotes = notesManager.listSharedNotes();
|
|
514
553
|
if (sharedNotes.length === 0) {
|
|
515
554
|
return successResponse("No shared notes found. You have no notes shared with collaborators.", { notes: [], count: 0 });
|
|
@@ -527,7 +566,7 @@ server.tool("list-shared-notes", {}, withErrorHandling(() => {
|
|
|
527
566
|
// Diagnostics Tools
|
|
528
567
|
// =============================================================================
|
|
529
568
|
// --- get-sync-status ---
|
|
530
|
-
server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
569
|
+
server.tool("get-sync-status", "Use when: checking whether iCloud sync is in progress before trusting read results.\nReturns: sync active/idle, pending upload count, and seconds since last change.\nDo not use when: you need note data — this is a read-only diagnostics tool.", {}, withErrorHandling(() => {
|
|
531
570
|
const status = getSyncStatus();
|
|
532
571
|
if (status.error) {
|
|
533
572
|
return successResponse(`⚠️ Sync status unknown: ${status.error}`, { ...status });
|
|
@@ -553,7 +592,7 @@ server.tool("get-sync-status", {}, withErrorHandling(() => {
|
|
|
553
592
|
return successResponse(lines.join("\n"), { ...status });
|
|
554
593
|
}, "Error checking sync status"));
|
|
555
594
|
// --- health-check ---
|
|
556
|
-
server.tool("health-check", {}, withErrorHandling(() => {
|
|
595
|
+
server.tool("health-check", "Use when: a quick check that Notes.app is reachable and (optionally) Full Disk Access is granted for checklist features.\nReturns: pass/fail per check.\nDo not use when: you need detailed, actionable setup diagnostics (use doctor).\nRead-only.", {}, withErrorHandling(() => {
|
|
557
596
|
const result = notesManager.healthCheck();
|
|
558
597
|
const statusIcon = result.healthy ? "✓" : "✗";
|
|
559
598
|
const statusText = result.healthy ? "All checks passed" : "Issues detected";
|
|
@@ -571,14 +610,14 @@ server.tool("health-check", {}, withErrorHandling(() => {
|
|
|
571
610
|
return successResponse(`${statusIcon} ${statusText}\n\n${checkLines}\n${fdaLine}`);
|
|
572
611
|
}, "Error running health check"));
|
|
573
612
|
// --- doctor ---
|
|
574
|
-
server.tool("doctor", {}, withErrorHandling(() => {
|
|
613
|
+
server.tool("doctor", "Use when: diagnosing setup problems (Notes.app automation permission, account state, Full Disk Access) with actionable guidance.\nReturns: a detailed report plus structured fields.\nDo not use when: you just need a quick pass/fail (health-check).\nRead-only.", {}, withErrorHandling(() => {
|
|
575
614
|
// Richer than health-check: Notes.app permission, account state, and Full
|
|
576
615
|
// Disk Access with actionable messages + structuredContent (#22).
|
|
577
616
|
const report = runDoctor(notesManager);
|
|
578
617
|
return successResponse(formatDoctorReport(report), { ...report });
|
|
579
618
|
}, "Error running doctor"));
|
|
580
619
|
// --- get-notes-stats ---
|
|
581
|
-
server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
620
|
+
server.tool("get-notes-stats", "Use when: summarizing the library — total notes, per-account/folder counts, and recent activity.\nReturns: aggregate statistics; flags partial coverage when some scopes were unreadable.\nDo not use when: you need individual notes (list-notes/search-notes).\nRead-only.", {}, withErrorHandling(() => {
|
|
582
621
|
const stats = notesManager.getNotesStats();
|
|
583
622
|
// Format the output
|
|
584
623
|
const lines = [];
|
|
@@ -614,7 +653,7 @@ server.tool("get-notes-stats", {}, withErrorHandling(() => {
|
|
|
614
653
|
return successResponse(lines.join("\n"), { ...stats });
|
|
615
654
|
}, "Error getting notes statistics"));
|
|
616
655
|
// --- list-attachments ---
|
|
617
|
-
server.tool("list-attachments", {
|
|
656
|
+
server.tool("list-attachments", "Use when: listing the attachments of one note, by id (preferred) or title.\nReturns: each attachment's name, content type, and id (use with save-attachment/fetch-attachment).\nDo not use when: you want the attachment bytes (fetch-attachment) or a file on disk (save-attachment).", {
|
|
618
657
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
619
658
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
620
659
|
account: z
|
|
@@ -654,7 +693,7 @@ server.tool("list-attachments", {
|
|
|
654
693
|
return successResponse(`Found ${attachments.length} attachment(s) in "${title}":\n${attachmentList}`, { attachments, count: attachments.length });
|
|
655
694
|
}, "Error listing attachments"));
|
|
656
695
|
// --- batch-delete-notes ---
|
|
657
|
-
server.tool("batch-delete-notes", {
|
|
696
|
+
server.tool("batch-delete-notes", "Use when: permanently deleting multiple notes by id in one call.\nReturns: per-id success/failure counts.\nDo not use when: deleting a single note (delete-note).\nSafety: requires explicit user confirmation; this is destructive and not undoable. Prefer search-notes/list-notes first to confirm the exact ids being deleted.", {
|
|
658
697
|
ids: z.array(z.string()).describe("Array of note IDs to delete"),
|
|
659
698
|
}, withErrorHandling(({ ids }) => {
|
|
660
699
|
if (ids.length === 0) {
|
|
@@ -673,7 +712,7 @@ server.tool("batch-delete-notes", {
|
|
|
673
712
|
return succeeded > 0 ? successResponse(lines.join("\n")) : errorResponse(lines.join("\n"));
|
|
674
713
|
}, "Error performing batch delete"));
|
|
675
714
|
// --- batch-move-notes ---
|
|
676
|
-
server.tool("batch-move-notes", {
|
|
715
|
+
server.tool("batch-move-notes", "Use when: moving multiple notes by id into one destination folder.\nReturns: per-id success/failure counts.\nDo not use when: moving a single note (move-note).\nNote: the destination folder must already exist (create-folder).", {
|
|
677
716
|
ids: z.array(z.string()).describe("Array of note IDs to move"),
|
|
678
717
|
folder: z.string().describe("Destination folder name"),
|
|
679
718
|
account: z
|
|
@@ -697,7 +736,7 @@ server.tool("batch-move-notes", {
|
|
|
697
736
|
return succeeded > 0 ? successResponse(lines.join("\n")) : errorResponse(lines.join("\n"));
|
|
698
737
|
}, "Error performing batch move"));
|
|
699
738
|
// --- save-attachment ---
|
|
700
|
-
server.tool("save-attachment", {
|
|
739
|
+
server.tool("save-attachment", "Use when: writing one note attachment to a file on disk.\nReturns: the saved path.\nDo not use when: you want the bytes in-memory as base64 (fetch-attachment).\nSafety: writes a file; savePath must be absolute and under the home directory, a temp dir, or /Volumes. Get the ids from list-attachments first.", {
|
|
701
740
|
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
702
741
|
attachmentId: z
|
|
703
742
|
.string()
|
|
@@ -719,7 +758,7 @@ server.tool("save-attachment", {
|
|
|
719
758
|
});
|
|
720
759
|
}, "Error saving attachment"));
|
|
721
760
|
// --- fetch-attachment ---
|
|
722
|
-
server.tool("fetch-attachment", {
|
|
761
|
+
server.tool("fetch-attachment", "Use when: retrieving one note attachment's bytes inline as base64 (no file written).\nReturns: name, content type, byte count, and base64 data.\nDo not use when: you want it saved to disk (save-attachment).\nNote: get the ids from list-attachments first.", {
|
|
723
762
|
noteId: z.string().min(1, "noteId is required").describe("CoreData note id (from search/list)"),
|
|
724
763
|
attachmentId: z
|
|
725
764
|
.string()
|
|
@@ -733,7 +772,7 @@ server.tool("fetch-attachment", {
|
|
|
733
772
|
return successResponse(`Fetched "${r.name ?? "attachment"}" (${r.contentType ?? "unknown type"}, ${r.bytes ?? 0} bytes) as base64.`, { name: r.name, contentType: r.contentType, bytes: r.bytes, base64: r.base64 });
|
|
734
773
|
}, "Error fetching attachment"));
|
|
735
774
|
// --- export-notes-json ---
|
|
736
|
-
server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
775
|
+
server.tool("export-notes-json", "Use when: exporting the entire notes library as structured JSON for backup or bulk processing.\nReturns: a summary plus the full JSON of all notes, folders, and accounts.\nDo not use when: you need a single note (get-note-content) — this reads everything and can be large.\nRead-only.", {}, withErrorHandling(() => {
|
|
737
776
|
const exportData = notesManager.exportNotesAsJson();
|
|
738
777
|
const { summary } = exportData;
|
|
739
778
|
return {
|
|
@@ -751,7 +790,7 @@ server.tool("export-notes-json", {}, withErrorHandling(() => {
|
|
|
751
790
|
};
|
|
752
791
|
}, "Error exporting notes"));
|
|
753
792
|
// --- get-note-markdown ---
|
|
754
|
-
server.tool("get-note-markdown", {
|
|
793
|
+
server.tool("get-note-markdown", "Use when: reading a note as Markdown, with checklist items annotated [x]/[ ] when Full Disk Access is granted.\nReturns: the note's Markdown.\nDo not use when: you need the raw HTML/plaintext body (get-note-content) or only metadata (get-note-details).\nNote: falls back to plain lists (no checkmarks) without Full Disk Access.", {
|
|
755
794
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
756
795
|
title: z.string().optional().describe("Note title (use id instead when available)"),
|
|
757
796
|
account: z
|
|
@@ -778,7 +817,7 @@ server.tool("get-note-markdown", {
|
|
|
778
817
|
return successResponse(markdown, { markdown });
|
|
779
818
|
}, "Error getting note as markdown"));
|
|
780
819
|
// --- get-checklist-state ---
|
|
781
|
-
server.tool("get-checklist-state", {
|
|
820
|
+
server.tool("get-checklist-state", "Use when: reading the checked/unchecked state of a note's checklist items, by id.\nReturns: each item's text and done state plus checked/total counts.\nDo not use when: you only have a title (get the id via search-notes first) or want the full body text (get-note-content).\nNote: requires Full Disk Access; reads the NoteStore database directly.", {
|
|
782
821
|
id: z.string().min(1, "Note ID is required. Use search-notes to find the note ID first."),
|
|
783
822
|
}, withErrorHandling(({ id }) => {
|
|
784
823
|
// Verify the note exists and is accessible
|
|
@@ -1207,25 +1207,23 @@ export class AppleNotesManager {
|
|
|
1207
1207
|
*/
|
|
1208
1208
|
listFolders(account) {
|
|
1209
1209
|
const targetAccount = this.resolveAccount(account);
|
|
1210
|
-
// Get each folder's ID, name,
|
|
1211
|
-
// Each line: "id\tname\tparentId" for subfolders, "id\tname" for top-level.
|
|
1210
|
+
// Get each folder's ID, name, parent ID, and shared state in a single AppleScript call.
|
|
1212
1211
|
// Using IDs enables correct tree building even with duplicate folder names.
|
|
1213
1212
|
const listCommand = `
|
|
1214
|
-
set folderList to
|
|
1213
|
+
set folderList to {}
|
|
1215
1214
|
set allFolders to every folder
|
|
1216
1215
|
repeat with f in allFolders
|
|
1217
1216
|
set fRef to contents of f
|
|
1218
1217
|
set cRef to container of fRef
|
|
1219
|
-
|
|
1220
|
-
set folderList to folderList & linefeed
|
|
1221
|
-
end if
|
|
1218
|
+
set parentId to ""
|
|
1222
1219
|
if class of cRef is folder then
|
|
1223
|
-
set
|
|
1224
|
-
else
|
|
1225
|
-
set folderList to folderList & (id of fRef) & tab & (name of fRef)
|
|
1220
|
+
set parentId to id of cRef
|
|
1226
1221
|
end if
|
|
1222
|
+
set sharedFlag to shared of fRef as text
|
|
1223
|
+
set end of folderList to (id of fRef) & ${AS_FIELD_SEP} & (name of fRef) & ${AS_FIELD_SEP} & parentId & ${AS_FIELD_SEP} & sharedFlag
|
|
1227
1224
|
end repeat
|
|
1228
|
-
|
|
1225
|
+
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
1226
|
+
return folderList as text
|
|
1229
1227
|
`;
|
|
1230
1228
|
const script = buildAccountScopedScript({ account: targetAccount }, listCommand);
|
|
1231
1229
|
const result = executeAppleScript(script);
|
|
@@ -1235,13 +1233,14 @@ export class AppleNotesManager {
|
|
|
1235
1233
|
if (!result.output.trim()) {
|
|
1236
1234
|
return [];
|
|
1237
1235
|
}
|
|
1238
|
-
|
|
1239
|
-
const entries = result.output.split(
|
|
1240
|
-
const parts = line.split("\t");
|
|
1236
|
+
const recordSeparator = result.output.includes(RECORD_SEP) ? RECORD_SEP : "\n";
|
|
1237
|
+
const entries = result.output.split(recordSeparator).map((line) => {
|
|
1238
|
+
const parts = line.includes(FIELD_SEP) ? line.split(FIELD_SEP) : line.split("\t");
|
|
1241
1239
|
return {
|
|
1242
1240
|
id: (parts[0] || "").trim(),
|
|
1243
1241
|
name: (parts[1] || "").trim(),
|
|
1244
1242
|
parentId: (parts[2] || "").trim(),
|
|
1243
|
+
shared: (parts[3] || "").trim().toLowerCase() === "true",
|
|
1245
1244
|
};
|
|
1246
1245
|
});
|
|
1247
1246
|
// Build an ID-to-entry map for efficient parent lookups
|
|
@@ -1264,6 +1263,7 @@ export class AppleNotesManager {
|
|
|
1264
1263
|
id: entry.id,
|
|
1265
1264
|
name: buildPath(entry),
|
|
1266
1265
|
account: targetAccount,
|
|
1266
|
+
shared: entry.shared,
|
|
1267
1267
|
}));
|
|
1268
1268
|
}
|
|
1269
1269
|
/**
|
|
@@ -1455,10 +1455,22 @@ export class AppleNotesManager {
|
|
|
1455
1455
|
* @returns Array of Account objects
|
|
1456
1456
|
*/
|
|
1457
1457
|
listAccounts() {
|
|
1458
|
-
// Coerce
|
|
1459
|
-
//
|
|
1458
|
+
// Coerce account records to text with control-char delimiters so names
|
|
1459
|
+
// containing commas or tabs can't split into phantom accounts (#18).
|
|
1460
1460
|
const listCommand = `
|
|
1461
|
-
set resultList to
|
|
1461
|
+
set resultList to {}
|
|
1462
|
+
repeat with a in accounts
|
|
1463
|
+
set aRef to contents of a
|
|
1464
|
+
set defaultFolderId to ""
|
|
1465
|
+
set defaultFolderName to ""
|
|
1466
|
+
try
|
|
1467
|
+
set fRef to default folder of aRef
|
|
1468
|
+
set defaultFolderId to id of fRef
|
|
1469
|
+
set defaultFolderName to name of fRef
|
|
1470
|
+
end try
|
|
1471
|
+
set upgradedFlag to upgraded of aRef as text
|
|
1472
|
+
set end of resultList to (id of aRef) & ${AS_FIELD_SEP} & (name of aRef) & ${AS_FIELD_SEP} & upgradedFlag & ${AS_FIELD_SEP} & defaultFolderId & ${AS_FIELD_SEP} & defaultFolderName
|
|
1473
|
+
end repeat
|
|
1462
1474
|
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
1463
1475
|
return resultList as text
|
|
1464
1476
|
`;
|
|
@@ -1467,11 +1479,130 @@ export class AppleNotesManager {
|
|
|
1467
1479
|
if (!result.success) {
|
|
1468
1480
|
throw new Error(`Failed to list accounts: ${result.error ?? "unknown error"}`);
|
|
1469
1481
|
}
|
|
1470
|
-
|
|
1482
|
+
return result.output
|
|
1471
1483
|
.split(RECORD_SEP)
|
|
1472
1484
|
.map((s) => s.trim())
|
|
1473
|
-
.filter((s) => s.length > 0)
|
|
1474
|
-
|
|
1485
|
+
.filter((s) => s.length > 0)
|
|
1486
|
+
.map((item) => {
|
|
1487
|
+
const parts = item.split(FIELD_SEP);
|
|
1488
|
+
if (parts.length === 1) {
|
|
1489
|
+
return { name: parts[0].trim() };
|
|
1490
|
+
}
|
|
1491
|
+
return {
|
|
1492
|
+
id: (parts[0] || "").trim(),
|
|
1493
|
+
name: (parts[1] || "").trim(),
|
|
1494
|
+
upgraded: (parts[2] || "").trim().toLowerCase() === "true",
|
|
1495
|
+
defaultFolderId: (parts[3] || "").trim() || undefined,
|
|
1496
|
+
defaultFolder: (parts[4] || "").trim() || undefined,
|
|
1497
|
+
};
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Gets the default account and folder used by Notes.app for new notes.
|
|
1502
|
+
*
|
|
1503
|
+
* @returns Default account and folder metadata
|
|
1504
|
+
*/
|
|
1505
|
+
getDefaultLocation() {
|
|
1506
|
+
const command = `
|
|
1507
|
+
set aRef to default account
|
|
1508
|
+
set fRef to default folder of aRef
|
|
1509
|
+
return (id of aRef) & ${AS_FIELD_SEP} & (name of aRef) & ${AS_FIELD_SEP} & (upgraded of aRef as text) & ${AS_FIELD_SEP} & (id of fRef) & ${AS_FIELD_SEP} & (name of fRef) & ${AS_FIELD_SEP} & (shared of fRef as text)
|
|
1510
|
+
`;
|
|
1511
|
+
const result = executeAppleScript(buildAppLevelScript(command));
|
|
1512
|
+
if (!result.success) {
|
|
1513
|
+
throw new Error(`Failed to get default Notes location: ${result.error ?? "unknown error"}`);
|
|
1514
|
+
}
|
|
1515
|
+
const parts = result.output.split(FIELD_SEP);
|
|
1516
|
+
if (parts.length < 6) {
|
|
1517
|
+
throw new Error(`Failed to parse default Notes location: ${result.output}`);
|
|
1518
|
+
}
|
|
1519
|
+
const accountName = (parts[1] || "").trim();
|
|
1520
|
+
return {
|
|
1521
|
+
account: {
|
|
1522
|
+
id: (parts[0] || "").trim(),
|
|
1523
|
+
name: accountName,
|
|
1524
|
+
upgraded: (parts[2] || "").trim().toLowerCase() === "true",
|
|
1525
|
+
defaultFolderId: (parts[3] || "").trim(),
|
|
1526
|
+
defaultFolder: (parts[4] || "").trim(),
|
|
1527
|
+
},
|
|
1528
|
+
folder: {
|
|
1529
|
+
id: (parts[3] || "").trim(),
|
|
1530
|
+
name: (parts[4] || "").trim(),
|
|
1531
|
+
account: accountName,
|
|
1532
|
+
shared: (parts[5] || "").trim().toLowerCase() === "true",
|
|
1533
|
+
},
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
/**
|
|
1537
|
+
* Lists the currently selected Notes in the Notes.app UI.
|
|
1538
|
+
*
|
|
1539
|
+
* @returns Array of selected notes, or an empty array when nothing is selected
|
|
1540
|
+
*/
|
|
1541
|
+
getSelectedNotes() {
|
|
1542
|
+
const command = `
|
|
1543
|
+
set selectedNotes to selection
|
|
1544
|
+
set noteList to {}
|
|
1545
|
+
repeat with n in selectedNotes
|
|
1546
|
+
set nRef to contents of n
|
|
1547
|
+
set createdDate to creation date of nRef
|
|
1548
|
+
set modifiedDate to modification date of nRef
|
|
1549
|
+
set createdParts to ${asDatePartsExpr("createdDate")}
|
|
1550
|
+
set modifiedParts to ${asDatePartsExpr("modifiedDate")}
|
|
1551
|
+
set folderName to ""
|
|
1552
|
+
set accountName to ""
|
|
1553
|
+
try
|
|
1554
|
+
set fRef to container of nRef
|
|
1555
|
+
set folderName to name of fRef
|
|
1556
|
+
set aRef to container of fRef
|
|
1557
|
+
set accountName to name of aRef
|
|
1558
|
+
end try
|
|
1559
|
+
set end of noteList to (id of nRef) & ${AS_FIELD_SEP} & (name of nRef) & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & (shared of nRef as text) & ${AS_FIELD_SEP} & (password protected of nRef as text) & ${AS_FIELD_SEP} & folderName & ${AS_FIELD_SEP} & accountName
|
|
1560
|
+
end repeat
|
|
1561
|
+
set AppleScript's text item delimiters to ${AS_RECORD_SEP}
|
|
1562
|
+
return noteList as text
|
|
1563
|
+
`;
|
|
1564
|
+
const result = executeAppleScript(buildAppLevelScript(command));
|
|
1565
|
+
if (!result.success) {
|
|
1566
|
+
throw new Error(`Failed to get selected notes: ${result.error ?? "unknown error"}`);
|
|
1567
|
+
}
|
|
1568
|
+
if (!result.output.trim()) {
|
|
1569
|
+
return [];
|
|
1570
|
+
}
|
|
1571
|
+
return result.output
|
|
1572
|
+
.split(RECORD_SEP)
|
|
1573
|
+
.filter((s) => s.trim())
|
|
1574
|
+
.map((item) => {
|
|
1575
|
+
const parts = item.split(FIELD_SEP);
|
|
1576
|
+
return {
|
|
1577
|
+
id: (parts[0] || "").trim(),
|
|
1578
|
+
title: (parts[1] || "").trim(),
|
|
1579
|
+
content: "",
|
|
1580
|
+
tags: [],
|
|
1581
|
+
created: parseAppleScriptDate((parts[2] || "").trim()),
|
|
1582
|
+
modified: parseAppleScriptDate((parts[3] || "").trim()),
|
|
1583
|
+
shared: (parts[4] || "").trim().toLowerCase() === "true",
|
|
1584
|
+
passwordProtected: (parts[5] || "").trim().toLowerCase() === "true",
|
|
1585
|
+
folder: (parts[6] || "").trim() || undefined,
|
|
1586
|
+
account: (parts[7] || "").trim() || undefined,
|
|
1587
|
+
};
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
/**
|
|
1591
|
+
* Reveals a note in the Notes.app UI by ID.
|
|
1592
|
+
*
|
|
1593
|
+
* @param id - CoreData URL identifier for the note
|
|
1594
|
+
* @param separately - Whether to open the note in a separate window
|
|
1595
|
+
* @returns true if Notes.app accepted the show command
|
|
1596
|
+
*/
|
|
1597
|
+
showNoteById(id, separately = false) {
|
|
1598
|
+
const safeId = sanitizeId(id);
|
|
1599
|
+
const separatelyClause = separately ? " separately true" : "";
|
|
1600
|
+
const result = executeAppleScript(buildAppLevelScript(`show note id "${safeId}"${separatelyClause}`));
|
|
1601
|
+
if (!result.success) {
|
|
1602
|
+
console.error(`Failed to show note with ID "${id}":`, result.error);
|
|
1603
|
+
return false;
|
|
1604
|
+
}
|
|
1605
|
+
return true;
|
|
1475
1606
|
}
|
|
1476
1607
|
// ===========================================================================
|
|
1477
1608
|
// Health Check
|
|
@@ -1738,8 +1869,17 @@ export class AppleNotesManager {
|
|
|
1738
1869
|
repeat with a in attachments of theNote
|
|
1739
1870
|
set attachId to id of a
|
|
1740
1871
|
set attachName to name of a
|
|
1741
|
-
set
|
|
1742
|
-
set
|
|
1872
|
+
set attachContentId to content identifier of a
|
|
1873
|
+
set attachUrl to ""
|
|
1874
|
+
try
|
|
1875
|
+
set attachUrl to URL of a as text
|
|
1876
|
+
end try
|
|
1877
|
+
set createdDate to creation date of a
|
|
1878
|
+
set modifiedDate to modification date of a
|
|
1879
|
+
set createdParts to ${asDatePartsExpr("createdDate")}
|
|
1880
|
+
set modifiedParts to ${asDatePartsExpr("modifiedDate")}
|
|
1881
|
+
set sharedFlag to shared of a as text
|
|
1882
|
+
set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
|
|
1743
1883
|
end repeat
|
|
1744
1884
|
set output to ""
|
|
1745
1885
|
repeat with item in attachmentList
|
|
@@ -1765,6 +1905,11 @@ export class AppleNotesManager {
|
|
|
1765
1905
|
id: parts[0].trim(),
|
|
1766
1906
|
name: parts[1].trim(),
|
|
1767
1907
|
contentType: parts[2].trim(),
|
|
1908
|
+
contentId: parts[2].trim() || undefined,
|
|
1909
|
+
url: parts[3]?.trim() || undefined,
|
|
1910
|
+
created: parts[4] ? parseAppleScriptDate(parts[4].trim()) : undefined,
|
|
1911
|
+
modified: parts[5] ? parseAppleScriptDate(parts[5].trim()) : undefined,
|
|
1912
|
+
shared: parts[6] ? parts[6].trim().toLowerCase() === "true" : undefined,
|
|
1768
1913
|
});
|
|
1769
1914
|
}
|
|
1770
1915
|
}
|
|
@@ -1784,13 +1929,22 @@ export class AppleNotesManager {
|
|
|
1784
1929
|
tell application "Notes"
|
|
1785
1930
|
tell account "${targetAccount}"
|
|
1786
1931
|
set theNote to note "${safeTitle}"
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1932
|
+
set attachmentList to {}
|
|
1933
|
+
repeat with a in attachments of theNote
|
|
1934
|
+
set attachId to id of a
|
|
1935
|
+
set attachName to name of a
|
|
1936
|
+
set attachContentId to content identifier of a
|
|
1937
|
+
set attachUrl to ""
|
|
1938
|
+
try
|
|
1939
|
+
set attachUrl to URL of a as text
|
|
1940
|
+
end try
|
|
1941
|
+
set createdDate to creation date of a
|
|
1942
|
+
set modifiedDate to modification date of a
|
|
1943
|
+
set createdParts to ${asDatePartsExpr("createdDate")}
|
|
1944
|
+
set modifiedParts to ${asDatePartsExpr("modifiedDate")}
|
|
1945
|
+
set sharedFlag to shared of a as text
|
|
1946
|
+
set end of attachmentList to attachId & ${AS_FIELD_SEP} & attachName & ${AS_FIELD_SEP} & attachContentId & ${AS_FIELD_SEP} & attachUrl & ${AS_FIELD_SEP} & createdParts & ${AS_FIELD_SEP} & modifiedParts & ${AS_FIELD_SEP} & sharedFlag
|
|
1947
|
+
end repeat
|
|
1794
1948
|
set output to ""
|
|
1795
1949
|
repeat with item in attachmentList
|
|
1796
1950
|
set output to output & item & ${AS_RECORD_SEP}
|
|
@@ -1816,6 +1970,11 @@ export class AppleNotesManager {
|
|
|
1816
1970
|
id: parts[0].trim(),
|
|
1817
1971
|
name: parts[1].trim(),
|
|
1818
1972
|
contentType: parts[2].trim(),
|
|
1973
|
+
contentId: parts[2].trim() || undefined,
|
|
1974
|
+
url: parts[3]?.trim() || undefined,
|
|
1975
|
+
created: parts[4] ? parseAppleScriptDate(parts[4].trim()) : undefined,
|
|
1976
|
+
modified: parts[5] ? parseAppleScriptDate(parts[5].trim()) : undefined,
|
|
1977
|
+
shared: parts[6] ? parts[6].trim().toLowerCase() === "true" : undefined,
|
|
1819
1978
|
});
|
|
1820
1979
|
}
|
|
1821
1980
|
}
|
|
@@ -1145,7 +1145,11 @@ describe("AppleNotesManager", () => {
|
|
|
1145
1145
|
it("returns array of Folder objects with paths", () => {
|
|
1146
1146
|
mockExecuteAppleScript.mockReturnValue({
|
|
1147
1147
|
success: true,
|
|
1148
|
-
output:
|
|
1148
|
+
output: [
|
|
1149
|
+
["id1", "Notes", "", "false"].join(F),
|
|
1150
|
+
["id2", "Archive", "", "false"].join(F),
|
|
1151
|
+
["id3", "Work", "", "true"].join(F),
|
|
1152
|
+
].join(R),
|
|
1149
1153
|
});
|
|
1150
1154
|
const folders = manager.listFolders();
|
|
1151
1155
|
expect(folders).toHaveLength(3);
|
|
@@ -1153,11 +1157,17 @@ describe("AppleNotesManager", () => {
|
|
|
1153
1157
|
expect(folders[1].name).toBe("Archive");
|
|
1154
1158
|
expect(folders[2].name).toBe("Work");
|
|
1155
1159
|
expect(folders[0].id).toBe("id1");
|
|
1160
|
+
expect(folders[2].shared).toBe(true);
|
|
1156
1161
|
});
|
|
1157
1162
|
it("includes parent folder in path", () => {
|
|
1158
1163
|
mockExecuteAppleScript.mockReturnValue({
|
|
1159
1164
|
success: true,
|
|
1160
|
-
output:
|
|
1165
|
+
output: [
|
|
1166
|
+
["id1", "Dev", "", "false"].join(F),
|
|
1167
|
+
["id2", "Accessibility", "id1", "false"].join(F),
|
|
1168
|
+
["id3", "Work", "", "false"].join(F),
|
|
1169
|
+
["id4", "Clients", "id3", "false"].join(F),
|
|
1170
|
+
].join(R),
|
|
1161
1171
|
});
|
|
1162
1172
|
const folders = manager.listFolders();
|
|
1163
1173
|
expect(folders).toHaveLength(4);
|
|
@@ -1169,7 +1179,13 @@ describe("AppleNotesManager", () => {
|
|
|
1169
1179
|
it("disambiguates duplicate folder names using IDs", () => {
|
|
1170
1180
|
mockExecuteAppleScript.mockReturnValue({
|
|
1171
1181
|
success: true,
|
|
1172
|
-
output:
|
|
1182
|
+
output: [
|
|
1183
|
+
["id1", "Finance", "", "false"].join(F),
|
|
1184
|
+
["id2", "Archive", "id1", "false"].join(F),
|
|
1185
|
+
["id3", "Travel", "", "false"].join(F),
|
|
1186
|
+
["id4", "Trips", "id3", "false"].join(F),
|
|
1187
|
+
["id5", "Archive", "id4", "false"].join(F),
|
|
1188
|
+
].join(R),
|
|
1173
1189
|
});
|
|
1174
1190
|
const folders = manager.listFolders();
|
|
1175
1191
|
expect(folders).toHaveLength(5);
|
|
@@ -1179,17 +1195,31 @@ describe("AppleNotesManager", () => {
|
|
|
1179
1195
|
it("escapes slashes in folder names", () => {
|
|
1180
1196
|
mockExecuteAppleScript.mockReturnValue({
|
|
1181
1197
|
success: true,
|
|
1182
|
-
output:
|
|
1198
|
+
output: [
|
|
1199
|
+
["id1", "Travel", "", "false"].join(F),
|
|
1200
|
+
["id2", "Spain/Portugal 2023", "id1", "false"].join(F),
|
|
1201
|
+
].join(R),
|
|
1183
1202
|
});
|
|
1184
1203
|
const folders = manager.listFolders();
|
|
1185
1204
|
expect(folders).toHaveLength(2);
|
|
1186
1205
|
expect(folders[0].name).toBe("Travel");
|
|
1187
1206
|
expect(folders[1].name).toBe("Travel/Spain\\/Portugal 2023");
|
|
1188
1207
|
});
|
|
1208
|
+
it("parses legacy tab/newline output (backward compat)", () => {
|
|
1209
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1210
|
+
success: true,
|
|
1211
|
+
output: "id1\tNotes\nid2\tArchive\tid1",
|
|
1212
|
+
});
|
|
1213
|
+
const folders = manager.listFolders();
|
|
1214
|
+
expect(folders).toHaveLength(2);
|
|
1215
|
+
expect(folders[0].name).toBe("Notes");
|
|
1216
|
+
expect(folders[1].name).toBe("Notes/Archive");
|
|
1217
|
+
expect(folders[1].shared).toBe(false);
|
|
1218
|
+
});
|
|
1189
1219
|
it("includes account in Folder objects", () => {
|
|
1190
1220
|
mockExecuteAppleScript.mockReturnValue({
|
|
1191
1221
|
success: true,
|
|
1192
|
-
output: "id1
|
|
1222
|
+
output: ["id1", "Notes", "", "false"].join(F),
|
|
1193
1223
|
});
|
|
1194
1224
|
const folders = manager.listFolders("Gmail");
|
|
1195
1225
|
expect(folders[0].account).toBe("Gmail");
|
|
@@ -1420,13 +1450,41 @@ describe("AppleNotesManager", () => {
|
|
|
1420
1450
|
it("returns array of Account objects", () => {
|
|
1421
1451
|
mockExecuteAppleScript.mockReturnValue({
|
|
1422
1452
|
success: true,
|
|
1423
|
-
output: [
|
|
1453
|
+
output: [
|
|
1454
|
+
["acc1", "iCloud", "true", "folder1", "Notes"].join(F),
|
|
1455
|
+
["acc2", "Gmail", "false", "folder2", "Inbox"].join(F),
|
|
1456
|
+
["acc3", "Exchange", "false", "", ""].join(F),
|
|
1457
|
+
].join(R),
|
|
1424
1458
|
});
|
|
1425
1459
|
const accounts = manager.listAccounts();
|
|
1426
1460
|
expect(accounts).toHaveLength(3);
|
|
1427
1461
|
expect(accounts[0].name).toBe("iCloud");
|
|
1428
1462
|
expect(accounts[1].name).toBe("Gmail");
|
|
1429
1463
|
expect(accounts[2].name).toBe("Exchange");
|
|
1464
|
+
expect(accounts[0].id).toBe("acc1");
|
|
1465
|
+
expect(accounts[0].upgraded).toBe(true);
|
|
1466
|
+
expect(accounts[0].defaultFolder).toBe("Notes");
|
|
1467
|
+
});
|
|
1468
|
+
it("parses legacy plain-name output (backward compat)", () => {
|
|
1469
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1470
|
+
success: true,
|
|
1471
|
+
output: ["iCloud", "Gmail"].join(R),
|
|
1472
|
+
});
|
|
1473
|
+
const accounts = manager.listAccounts();
|
|
1474
|
+
expect(accounts).toHaveLength(2);
|
|
1475
|
+
expect(accounts[0]).toEqual({ name: "iCloud" });
|
|
1476
|
+
expect(accounts[1]).toEqual({ name: "Gmail" });
|
|
1477
|
+
});
|
|
1478
|
+
it("handles account records with empty fields", () => {
|
|
1479
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1480
|
+
success: true,
|
|
1481
|
+
output: ["", "", "", "", ""].join(F),
|
|
1482
|
+
});
|
|
1483
|
+
const accounts = manager.listAccounts();
|
|
1484
|
+
expect(accounts).toHaveLength(1);
|
|
1485
|
+
expect(accounts[0].name).toBe("");
|
|
1486
|
+
expect(accounts[0].upgraded).toBe(false);
|
|
1487
|
+
expect(accounts[0].defaultFolderId).toBeUndefined();
|
|
1430
1488
|
});
|
|
1431
1489
|
it("throws on failure rather than returning empty (#19)", () => {
|
|
1432
1490
|
mockExecuteAppleScript.mockReturnValue({
|
|
@@ -1437,6 +1495,126 @@ describe("AppleNotesManager", () => {
|
|
|
1437
1495
|
expect(() => manager.listAccounts()).toThrow(/Notes.app not available/);
|
|
1438
1496
|
});
|
|
1439
1497
|
});
|
|
1498
|
+
describe("getDefaultLocation", () => {
|
|
1499
|
+
it("returns default account and folder metadata", () => {
|
|
1500
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1501
|
+
success: true,
|
|
1502
|
+
output: ["acc1", "iCloud", "true", "folder1", "Notes", "false"].join(F),
|
|
1503
|
+
});
|
|
1504
|
+
const location = manager.getDefaultLocation();
|
|
1505
|
+
expect(location.account).toMatchObject({
|
|
1506
|
+
id: "acc1",
|
|
1507
|
+
name: "iCloud",
|
|
1508
|
+
upgraded: true,
|
|
1509
|
+
defaultFolderId: "folder1",
|
|
1510
|
+
defaultFolder: "Notes",
|
|
1511
|
+
});
|
|
1512
|
+
expect(location.folder).toMatchObject({
|
|
1513
|
+
id: "folder1",
|
|
1514
|
+
name: "Notes",
|
|
1515
|
+
account: "iCloud",
|
|
1516
|
+
shared: false,
|
|
1517
|
+
});
|
|
1518
|
+
});
|
|
1519
|
+
it("throws when default location output cannot be parsed", () => {
|
|
1520
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1521
|
+
success: true,
|
|
1522
|
+
output: "bad-output",
|
|
1523
|
+
});
|
|
1524
|
+
expect(() => manager.getDefaultLocation()).toThrow(/parse default Notes location/);
|
|
1525
|
+
});
|
|
1526
|
+
it("throws when AppleScript fails", () => {
|
|
1527
|
+
mockExecuteAppleScript.mockReturnValue({ success: false, output: "", error: "boom" });
|
|
1528
|
+
expect(() => manager.getDefaultLocation()).toThrow(/Failed to get default Notes location/);
|
|
1529
|
+
});
|
|
1530
|
+
it("handles empty fields in default location output", () => {
|
|
1531
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1532
|
+
success: true,
|
|
1533
|
+
output: ["", "", "", "", "", ""].join(F),
|
|
1534
|
+
});
|
|
1535
|
+
const location = manager.getDefaultLocation();
|
|
1536
|
+
expect(location.account.name).toBe("");
|
|
1537
|
+
expect(location.account.upgraded).toBe(false);
|
|
1538
|
+
expect(location.folder.id).toBe("");
|
|
1539
|
+
expect(location.folder.shared).toBe(false);
|
|
1540
|
+
});
|
|
1541
|
+
});
|
|
1542
|
+
describe("getSelectedNotes", () => {
|
|
1543
|
+
it("returns selected note metadata", () => {
|
|
1544
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1545
|
+
success: true,
|
|
1546
|
+
output: [
|
|
1547
|
+
[
|
|
1548
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1549
|
+
"Selected Note",
|
|
1550
|
+
"2026-6-22-14-30-0",
|
|
1551
|
+
"2026-6-22-14-35-0",
|
|
1552
|
+
"false",
|
|
1553
|
+
"false",
|
|
1554
|
+
"Notes",
|
|
1555
|
+
"iCloud",
|
|
1556
|
+
].join(F),
|
|
1557
|
+
].join(R),
|
|
1558
|
+
});
|
|
1559
|
+
const notes = manager.getSelectedNotes();
|
|
1560
|
+
expect(notes).toHaveLength(1);
|
|
1561
|
+
expect(notes[0]).toMatchObject({
|
|
1562
|
+
id: "x-coredata://ABC/ICNote/p1",
|
|
1563
|
+
title: "Selected Note",
|
|
1564
|
+
shared: false,
|
|
1565
|
+
passwordProtected: false,
|
|
1566
|
+
folder: "Notes",
|
|
1567
|
+
account: "iCloud",
|
|
1568
|
+
});
|
|
1569
|
+
expect(notes[0].created.getFullYear()).toBe(2026);
|
|
1570
|
+
});
|
|
1571
|
+
it("returns an empty array when no note is selected", () => {
|
|
1572
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1573
|
+
success: true,
|
|
1574
|
+
output: "",
|
|
1575
|
+
});
|
|
1576
|
+
expect(manager.getSelectedNotes()).toEqual([]);
|
|
1577
|
+
});
|
|
1578
|
+
it("throws when AppleScript fails", () => {
|
|
1579
|
+
mockExecuteAppleScript.mockReturnValue({ success: false, output: "", error: "boom" });
|
|
1580
|
+
expect(() => manager.getSelectedNotes()).toThrow(/Failed to get selected notes/);
|
|
1581
|
+
});
|
|
1582
|
+
it("handles selected notes with empty optional fields", () => {
|
|
1583
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1584
|
+
success: true,
|
|
1585
|
+
output: ["", "", "", "", "", "", "", ""].join(F),
|
|
1586
|
+
});
|
|
1587
|
+
const notes = manager.getSelectedNotes();
|
|
1588
|
+
expect(notes).toHaveLength(1);
|
|
1589
|
+
expect(notes[0].id).toBe("");
|
|
1590
|
+
expect(notes[0].shared).toBe(false);
|
|
1591
|
+
expect(notes[0].passwordProtected).toBe(false);
|
|
1592
|
+
expect(notes[0].folder).toBeUndefined();
|
|
1593
|
+
expect(notes[0].account).toBeUndefined();
|
|
1594
|
+
});
|
|
1595
|
+
});
|
|
1596
|
+
describe("showNoteById", () => {
|
|
1597
|
+
it("shows a note by id", () => {
|
|
1598
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1599
|
+
success: true,
|
|
1600
|
+
output: "",
|
|
1601
|
+
});
|
|
1602
|
+
expect(manager.showNoteById("x-coredata://ABC/ICNote/p1")).toBe(true);
|
|
1603
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('show note id "x-coredata://ABC/ICNote/p1"'));
|
|
1604
|
+
});
|
|
1605
|
+
it("can request a separate window", () => {
|
|
1606
|
+
mockExecuteAppleScript.mockReturnValue({
|
|
1607
|
+
success: true,
|
|
1608
|
+
output: "",
|
|
1609
|
+
});
|
|
1610
|
+
manager.showNoteById("x-coredata://ABC/ICNote/p1", true);
|
|
1611
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining("separately true"));
|
|
1612
|
+
});
|
|
1613
|
+
it("returns false when Notes.app rejects the show command", () => {
|
|
1614
|
+
mockExecuteAppleScript.mockReturnValue({ success: false, output: "", error: "no such note" });
|
|
1615
|
+
expect(manager.showNoteById("x-coredata://ABC/ICNote/p1")).toBe(false);
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1440
1618
|
// ---------------------------------------------------------------------------
|
|
1441
1619
|
// Health Check
|
|
1442
1620
|
// ---------------------------------------------------------------------------
|
|
@@ -1620,16 +1798,45 @@ describe("AppleNotesManager", () => {
|
|
|
1620
1798
|
});
|
|
1621
1799
|
const attachments = manager.listAttachmentsById("x-coredata://ABC/ICNote/p123");
|
|
1622
1800
|
expect(attachments).toHaveLength(2);
|
|
1623
|
-
expect(attachments[0]).
|
|
1801
|
+
expect(attachments[0]).toMatchObject({
|
|
1624
1802
|
id: "x-coredata://ABC/ICAttachment/p1",
|
|
1625
1803
|
name: "photo.jpg",
|
|
1626
1804
|
contentType: "public.jpeg",
|
|
1805
|
+
contentId: "public.jpeg",
|
|
1627
1806
|
});
|
|
1628
|
-
expect(attachments[1]).
|
|
1807
|
+
expect(attachments[1]).toMatchObject({
|
|
1629
1808
|
id: "x-coredata://ABC/ICAttachment/p2",
|
|
1630
1809
|
name: "document.pdf",
|
|
1631
1810
|
contentType: "com.adobe.pdf",
|
|
1811
|
+
contentId: "com.adobe.pdf",
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
it("parses richer attachment metadata when present", () => {
|
|
1815
|
+
mockExecuteAppleScript.mockReturnValueOnce({
|
|
1816
|
+
success: true,
|
|
1817
|
+
output: [
|
|
1818
|
+
[
|
|
1819
|
+
"x-coredata://ABC/ICAttachment/p1",
|
|
1820
|
+
"site.webloc",
|
|
1821
|
+
"cid:123",
|
|
1822
|
+
"https://example.com",
|
|
1823
|
+
"2026-6-22-10-0-0",
|
|
1824
|
+
"2026-6-22-11-0-0",
|
|
1825
|
+
"true",
|
|
1826
|
+
].join(F),
|
|
1827
|
+
].join(R),
|
|
1828
|
+
});
|
|
1829
|
+
const attachments = manager.listAttachmentsById("x-coredata://ABC/ICNote/p123");
|
|
1830
|
+
expect(attachments[0]).toMatchObject({
|
|
1831
|
+
id: "x-coredata://ABC/ICAttachment/p1",
|
|
1832
|
+
name: "site.webloc",
|
|
1833
|
+
contentType: "cid:123",
|
|
1834
|
+
contentId: "cid:123",
|
|
1835
|
+
url: "https://example.com",
|
|
1836
|
+
shared: true,
|
|
1632
1837
|
});
|
|
1838
|
+
expect(attachments[0].created?.getFullYear()).toBe(2026);
|
|
1839
|
+
expect(attachments[0].modified?.getHours()).toBe(11);
|
|
1633
1840
|
});
|
|
1634
1841
|
it("returns empty array when note has no attachments", () => {
|
|
1635
1842
|
mockExecuteAppleScript.mockReturnValueOnce({ success: true, output: "" });
|
|
@@ -1659,10 +1866,11 @@ describe("AppleNotesManager", () => {
|
|
|
1659
1866
|
});
|
|
1660
1867
|
const attachments = manager.listAttachments("My Note");
|
|
1661
1868
|
expect(attachments).toHaveLength(1);
|
|
1662
|
-
expect(attachments[0]).
|
|
1869
|
+
expect(attachments[0]).toMatchObject({
|
|
1663
1870
|
id: "attach-id",
|
|
1664
1871
|
name: "image.png",
|
|
1665
1872
|
contentType: "public.png",
|
|
1873
|
+
contentId: "public.png",
|
|
1666
1874
|
});
|
|
1667
1875
|
});
|
|
1668
1876
|
it("uses specified account", () => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "2.1.
|
|
4
|
-
"description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and
|
|
3
|
+
"version": "2.1.2",
|
|
4
|
+
"description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude and other AI assistants",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"types": "build/index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"format": "prettier --write src",
|
|
28
28
|
"format:check": "prettier --check src",
|
|
29
29
|
"typecheck": "tsc --noEmit",
|
|
30
|
-
"version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin
|
|
30
|
+
"version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin .agents/plugins codex .hermes-plugin .antigravity-plugin",
|
|
31
31
|
"prepublishOnly": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"prepare": "husky; npm run build"
|
|
33
33
|
},
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vitest": "^2.0.0"
|
|
82
82
|
},
|
|
83
83
|
"volta": {
|
|
84
|
-
"node": "
|
|
84
|
+
"node": "24.17.0"
|
|
85
85
|
},
|
|
86
86
|
"lint-staged": {
|
|
87
87
|
"src/**/*.ts": [
|