apple-notes-mcp 1.4.0 → 1.4.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
CHANGED
|
@@ -757,6 +757,29 @@ All other tools work normally without Full Disk Access. Only checklist state fea
|
|
|
757
757
|
| Limited rich formatting | Use `format: "html"` on create/update for headings, lists, bold, code blocks; some complex formatting may not render |
|
|
758
758
|
| Title matching | Most operations require exact title matches |
|
|
759
759
|
| Checklist state | Requires Full Disk Access to read done/undone state from the database |
|
|
760
|
+
| Checklist **creation** | Not supported. AppleScript's `body of note` setter strips `<input type="checkbox">` and ignores any checklist-styling CSS class. Apple Notes stores checklist items as a protobuf paragraph style (`style_type=103`) that AppleScript doesn't expose, and the SQLite database is read-only. See [Creating Checklists](#creating-checklists) below for the workaround. |
|
|
761
|
+
|
|
762
|
+
### Creating Checklists
|
|
763
|
+
|
|
764
|
+
**There is no programmatic way to create a true Apple Notes checklist via AppleScript** — and therefore no way via this MCP server. This is an Apple limitation, not a bug.
|
|
765
|
+
|
|
766
|
+
When a note is created or updated via AppleScript:
|
|
767
|
+
|
|
768
|
+
| You send | What Notes.app actually renders |
|
|
769
|
+
|----------|--------------------------------|
|
|
770
|
+
| `<input type="checkbox"> Item` | `Item` (the `<input>` tag is stripped) |
|
|
771
|
+
| `<ul class="checklist"><li>Item</li></ul>` | A plain bulleted list — the `checklist` class is dropped |
|
|
772
|
+
| Markdown `- [ ] Item` (in `plaintext` mode) | The literal text `- [ ] Item` |
|
|
773
|
+
|
|
774
|
+
Apple Notes stores checklists as a paragraph style (`style_type=103`) inside a gzipped protobuf blob in the `NoteStore.sqlite` database. AppleScript's note `body` interface does not expose paragraph styles, and writing directly to the live database is unsafe.
|
|
775
|
+
|
|
776
|
+
**Workarounds:**
|
|
777
|
+
|
|
778
|
+
1. **Create the note with bulleted list items, then convert manually in Notes.app.** Select the items and press <kbd>⇧⌘L</kbd> (or **Format → Checklist**). This converts the list in place and the resulting checklist will be readable by `get-checklist-state` and annotated by `get-note-markdown`.
|
|
779
|
+
2. **Use the Apple Shortcuts app** to script the checklist creation, since Shortcuts can manipulate Notes content at a higher level than AppleScript.
|
|
780
|
+
3. **Read-only checklist support is fully implemented** — once a checklist exists (created manually or by another app), `get-checklist-state` and `get-note-markdown` will read its done/undone state correctly (with Full Disk Access).
|
|
781
|
+
|
|
782
|
+
If you need to *track* todos programmatically and don't strictly need them rendered as Apple Notes checklist UI, plain markdown-style `- [ ] item` / `- [x] item` lines in a `plaintext` note are a reasonable alternative — they are searchable, human-readable, and can be parsed by downstream tooling.
|
|
760
783
|
|
|
761
784
|
### Backslash Escaping (Important for AI Agents)
|
|
762
785
|
|
|
@@ -812,7 +835,7 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
812
835
|
```bash
|
|
813
836
|
npm install # Install dependencies
|
|
814
837
|
npm run build # Compile TypeScript
|
|
815
|
-
npm test # Run test suite (
|
|
838
|
+
npm test # Run test suite (313 tests)
|
|
816
839
|
npm run lint # Check code style
|
|
817
840
|
npm run format # Format code
|
|
818
841
|
```
|
|
@@ -838,4 +861,6 @@ Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for gui
|
|
|
838
861
|
|
|
839
862
|
## Related Projects
|
|
840
863
|
|
|
841
|
-
- [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp)
|
|
864
|
+
- [apple-mail-mcp](https://github.com/sweetrb/apple-mail-mcp) — MCP server for Apple Mail
|
|
865
|
+
- [apple-numbers-mcp](https://github.com/sweetrb/apple-numbers-mcp) — MCP server for Apple Numbers spreadsheets
|
|
866
|
+
- [apple-photos-mcp](https://github.com/sweetrb/apple-photos-mcp) — MCP server for Apple Photos
|
package/build/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import { z } from "zod";
|
|
|
26
26
|
import { AppleNotesManager } from "./services/appleNotesManager.js";
|
|
27
27
|
import { getSyncStatus, withSyncAwarenessSync } from "./utils/syncDetection.js";
|
|
28
28
|
import { getChecklistItems, hasFullDiskAccess } from "./utils/checklistParser.js";
|
|
29
|
+
import { detectChecklistAttempt } from "./utils/contentWarnings.js";
|
|
29
30
|
// Read version from package.json to keep it in sync
|
|
30
31
|
const require = createRequire(import.meta.url);
|
|
31
32
|
const { version } = require("../package.json");
|
|
@@ -112,7 +113,10 @@ const folderNameSchema = {
|
|
|
112
113
|
// --- create-note ---
|
|
113
114
|
server.tool("create-note", {
|
|
114
115
|
title: z.string().min(1, "Title is required"),
|
|
115
|
-
content: z
|
|
116
|
+
content: z
|
|
117
|
+
.string()
|
|
118
|
+
.min(1, "Content is required")
|
|
119
|
+
.describe('Note body. AppleScript cannot create true Apple Notes checklists — `<input type="checkbox">`, checklist CSS classes, and markdown `- [ ]` lines do not render as checkable items. To produce a checklist, create the note with a plain `<ul>` or `- ` list and convert it in Notes.app with ⇧⌘L.'),
|
|
116
120
|
format: z
|
|
117
121
|
.enum(["plaintext", "html"])
|
|
118
122
|
.optional()
|
|
@@ -129,7 +133,8 @@ server.tool("create-note", {
|
|
|
129
133
|
if (!note) {
|
|
130
134
|
return errorResponse(`Failed to create note "${title}". Check that Notes.app is configured and accessible.`);
|
|
131
135
|
}
|
|
132
|
-
|
|
136
|
+
const checklistWarning = detectChecklistAttempt(content) ?? "";
|
|
137
|
+
return successResponse(`Note created: "${note.title}" [id: ${note.id}]${checklistWarning}`);
|
|
133
138
|
}, "Error creating note"));
|
|
134
139
|
// --- search-notes ---
|
|
135
140
|
server.tool("search-notes", {
|
|
@@ -261,7 +266,10 @@ server.tool("update-note", {
|
|
|
261
266
|
id: z.string().optional().describe("Note ID (preferred - more reliable than title)"),
|
|
262
267
|
title: z.string().optional().describe("Current note title (use id instead when available)"),
|
|
263
268
|
newTitle: z.string().optional().describe("New title for the note"),
|
|
264
|
-
newContent: z
|
|
269
|
+
newContent: z
|
|
270
|
+
.string()
|
|
271
|
+
.min(1, "New content is required")
|
|
272
|
+
.describe("New note body. AppleScript cannot produce true Apple Notes checklists; checkbox inputs and `- [ ]` markdown do not render as checkable items. Use a plain list and convert in Notes.app with ⇧⌘L."),
|
|
265
273
|
format: z
|
|
266
274
|
.enum(["plaintext", "html"])
|
|
267
275
|
.optional()
|
|
@@ -291,7 +299,8 @@ server.tool("update-note", {
|
|
|
291
299
|
const sharedWarning = note.shared
|
|
292
300
|
? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
|
|
293
301
|
: "";
|
|
294
|
-
|
|
302
|
+
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
303
|
+
return successResponse(`Note updated: "${displayTitle}"${sharedWarning}${checklistWarning}`);
|
|
295
304
|
}
|
|
296
305
|
// Fall back to title-based update
|
|
297
306
|
if (!title) {
|
|
@@ -314,7 +323,8 @@ server.tool("update-note", {
|
|
|
314
323
|
const sharedWarning = note.shared
|
|
315
324
|
? "\n\n⚠️ This note is shared with collaborators. Your changes will be visible to them."
|
|
316
325
|
: "";
|
|
317
|
-
|
|
326
|
+
const checklistWarning = detectChecklistAttempt(newContent) ?? "";
|
|
327
|
+
return successResponse(`Note updated: "${finalTitle}"${sharedWarning}${checklistWarning}`);
|
|
318
328
|
}, "Error updating note"));
|
|
319
329
|
// --- delete-note ---
|
|
320
330
|
server.tool("delete-note", {
|
|
@@ -454,10 +464,16 @@ server.tool("list-folders", {
|
|
|
454
464
|
return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}${syncNote}`);
|
|
455
465
|
}, "Error listing folders"));
|
|
456
466
|
// --- create-folder ---
|
|
457
|
-
server.tool("create-folder",
|
|
467
|
+
server.tool("create-folder", {
|
|
468
|
+
name: z
|
|
469
|
+
.string()
|
|
470
|
+
.min(1, "Folder name is required")
|
|
471
|
+
.describe('Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'),
|
|
472
|
+
account: z.string().optional().describe("Account name (defaults to iCloud)"),
|
|
473
|
+
}, withErrorHandling(({ name, account }) => {
|
|
458
474
|
const folder = notesManager.createFolder(name, account);
|
|
459
475
|
if (!folder) {
|
|
460
|
-
return errorResponse(`Failed to create folder "${name}"
|
|
476
|
+
return errorResponse(`Failed to create folder "${name}".`);
|
|
461
477
|
}
|
|
462
478
|
return successResponse(`Folder created: "${folder.name}"`);
|
|
463
479
|
}, "Error creating folder"));
|
|
@@ -539,11 +539,11 @@ export class AppleNotesManager {
|
|
|
539
539
|
let createCommand;
|
|
540
540
|
if (folder) {
|
|
541
541
|
// Create note in specific folder (supports nested paths like "Work/Clients")
|
|
542
|
+
// Note: We avoid `set newNote` + `return id of newNote` because AppleScript
|
|
543
|
+
// fails to resolve the note reference in deeply nested folder contexts (-1728).
|
|
544
|
+
// The implicit return from `make new note` includes the ID which we parse.
|
|
542
545
|
const folderRef = buildFolderReference(folder);
|
|
543
|
-
createCommand = `
|
|
544
|
-
set newNote to make new note at ${folderRef} with properties {body:"${safeBody}"}
|
|
545
|
-
return id of newNote
|
|
546
|
-
`;
|
|
546
|
+
createCommand = `make new note at ${folderRef} with properties {body:"${safeBody}"}`;
|
|
547
547
|
}
|
|
548
548
|
else {
|
|
549
549
|
// Create note in default location
|
|
@@ -1194,16 +1194,52 @@ export class AppleNotesManager {
|
|
|
1194
1194
|
*/
|
|
1195
1195
|
createFolder(name, account) {
|
|
1196
1196
|
const targetAccount = this.resolveAccount(account);
|
|
1197
|
-
const
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const result = executeAppleScript(script);
|
|
1201
|
-
if (!result.success) {
|
|
1202
|
-
console.error(`Failed to create folder "${name}":`, result.error);
|
|
1197
|
+
const parts = splitFolderPath(name);
|
|
1198
|
+
if (parts.length === 0) {
|
|
1199
|
+
console.error(`Invalid folder name: "${name}"`);
|
|
1203
1200
|
return null;
|
|
1204
1201
|
}
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1202
|
+
// Create each segment of the path, checking existence first to avoid duplicates.
|
|
1203
|
+
// For "A/B/C": ensure "A" exists, then "A/B", then "A/B/C".
|
|
1204
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1205
|
+
const currentPath = parts
|
|
1206
|
+
.slice(0, i + 1)
|
|
1207
|
+
.map((p) => escapeFolderName(p))
|
|
1208
|
+
.join("/");
|
|
1209
|
+
const currentRef = buildFolderReference(currentPath);
|
|
1210
|
+
// Check if this folder already exists
|
|
1211
|
+
const checkScript = buildAccountScopedScript({ account: targetAccount }, `return id of ${currentRef}`);
|
|
1212
|
+
const checkResult = executeAppleScript(checkScript);
|
|
1213
|
+
if (checkResult.success) {
|
|
1214
|
+
// Folder exists, move to next segment
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
// Folder doesn't exist — create it
|
|
1218
|
+
const segmentName = escapeForAppleScript(parts[i]);
|
|
1219
|
+
let createCommand;
|
|
1220
|
+
if (i === 0) {
|
|
1221
|
+
createCommand = `make new folder with properties {name:"${segmentName}"}`;
|
|
1222
|
+
}
|
|
1223
|
+
else {
|
|
1224
|
+
const parentPath = parts
|
|
1225
|
+
.slice(0, i)
|
|
1226
|
+
.map((p) => escapeFolderName(p))
|
|
1227
|
+
.join("/");
|
|
1228
|
+
const parentRef = buildFolderReference(parentPath);
|
|
1229
|
+
createCommand = `make new folder at ${parentRef} with properties {name:"${segmentName}"}`;
|
|
1230
|
+
}
|
|
1231
|
+
const script = buildAccountScopedScript({ account: targetAccount }, createCommand);
|
|
1232
|
+
const result = executeAppleScript(script);
|
|
1233
|
+
if (!result.success) {
|
|
1234
|
+
console.error(`Failed to create folder "${name}":`, result.error);
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
// Get the ID of the final (deepest) folder
|
|
1239
|
+
const fullRef = buildFolderReference(name);
|
|
1240
|
+
const idScript = buildAccountScopedScript({ account: targetAccount }, `return id of ${fullRef}`);
|
|
1241
|
+
const idResult = executeAppleScript(idScript);
|
|
1242
|
+
const folderId = idResult.success ? extractCoreDataId(idResult.output, "folder") : "";
|
|
1207
1243
|
return {
|
|
1208
1244
|
id: folderId,
|
|
1209
1245
|
name,
|
|
@@ -1047,7 +1047,16 @@ describe("AppleNotesManager", () => {
|
|
|
1047
1047
|
});
|
|
1048
1048
|
describe("createFolder", () => {
|
|
1049
1049
|
it("returns Folder object on success", () => {
|
|
1050
|
-
mockExecuteAppleScript
|
|
1050
|
+
mockExecuteAppleScript
|
|
1051
|
+
// Check existence — folder doesn't exist
|
|
1052
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1053
|
+
// Create the folder
|
|
1054
|
+
.mockReturnValueOnce({
|
|
1055
|
+
success: true,
|
|
1056
|
+
output: "folder id x-coredata://ABC123/ICFolder/p456",
|
|
1057
|
+
})
|
|
1058
|
+
// Get ID of created folder
|
|
1059
|
+
.mockReturnValueOnce({
|
|
1051
1060
|
success: true,
|
|
1052
1061
|
output: "folder id x-coredata://ABC123/ICFolder/p456",
|
|
1053
1062
|
});
|
|
@@ -1056,15 +1065,82 @@ describe("AppleNotesManager", () => {
|
|
|
1056
1065
|
expect(result?.name).toBe("New Project");
|
|
1057
1066
|
expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p456");
|
|
1058
1067
|
});
|
|
1059
|
-
it("returns
|
|
1060
|
-
mockExecuteAppleScript
|
|
1068
|
+
it("returns existing folder without creating duplicate", () => {
|
|
1069
|
+
mockExecuteAppleScript
|
|
1070
|
+
// Check existence — folder already exists
|
|
1071
|
+
.mockReturnValueOnce({
|
|
1072
|
+
success: true,
|
|
1073
|
+
output: "x-coredata://ABC123/ICFolder/p789",
|
|
1074
|
+
})
|
|
1075
|
+
// Get ID of existing folder
|
|
1076
|
+
.mockReturnValueOnce({
|
|
1077
|
+
success: true,
|
|
1078
|
+
output: "folder id x-coredata://ABC123/ICFolder/p789",
|
|
1079
|
+
});
|
|
1080
|
+
const result = manager.createFolder("Existing Folder");
|
|
1081
|
+
expect(result).not.toBeNull();
|
|
1082
|
+
expect(result?.name).toBe("Existing Folder");
|
|
1083
|
+
expect(result?.id).toBe("x-coredata://ABC123/ICFolder/p789");
|
|
1084
|
+
// Should only have 2 calls (check + get ID), no create call
|
|
1085
|
+
expect(mockExecuteAppleScript).toHaveBeenCalledTimes(2);
|
|
1086
|
+
});
|
|
1087
|
+
it("returns null on genuine failure", () => {
|
|
1088
|
+
mockExecuteAppleScript
|
|
1089
|
+
// Check existence — doesn't exist
|
|
1090
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1091
|
+
// Create fails
|
|
1092
|
+
.mockReturnValueOnce({
|
|
1061
1093
|
success: false,
|
|
1062
1094
|
output: "",
|
|
1063
|
-
error: "
|
|
1095
|
+
error: "Permission denied",
|
|
1064
1096
|
});
|
|
1065
|
-
const result = manager.createFolder("
|
|
1097
|
+
const result = manager.createFolder("Restricted Folder");
|
|
1066
1098
|
expect(result).toBeNull();
|
|
1067
1099
|
});
|
|
1100
|
+
it("creates nested folder path", () => {
|
|
1101
|
+
mockExecuteAppleScript
|
|
1102
|
+
// Check "Retro Tech" — doesn't exist
|
|
1103
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1104
|
+
// Create "Retro Tech"
|
|
1105
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p1" })
|
|
1106
|
+
// Check "Retro Tech/PC" — doesn't exist
|
|
1107
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1108
|
+
// Create "PC" inside "Retro Tech"
|
|
1109
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" })
|
|
1110
|
+
// Check "Retro Tech/PC/CPUs" — doesn't exist
|
|
1111
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1112
|
+
// Create "CPUs" inside "Retro Tech/PC"
|
|
1113
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p3" })
|
|
1114
|
+
// Get ID of final folder
|
|
1115
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p3" });
|
|
1116
|
+
const result = manager.createFolder("Retro Tech/PC/CPUs");
|
|
1117
|
+
expect(result).not.toBeNull();
|
|
1118
|
+
expect(result?.name).toBe("Retro Tech/PC/CPUs");
|
|
1119
|
+
expect(result?.id).toBe("x-coredata://A/ICFolder/p3");
|
|
1120
|
+
// Verify the create commands (calls at index 1, 3, 5)
|
|
1121
|
+
const calls = mockExecuteAppleScript.mock.calls;
|
|
1122
|
+
expect(calls[1][0]).toContain('make new folder with properties {name:"Retro Tech"}');
|
|
1123
|
+
expect(calls[3][0]).toContain('make new folder at folder "Retro Tech" with properties {name:"PC"}');
|
|
1124
|
+
expect(calls[5][0]).toContain('make new folder at folder "PC" of folder "Retro Tech" with properties {name:"CPUs"}');
|
|
1125
|
+
});
|
|
1126
|
+
it("skips existing intermediate folders in nested path", () => {
|
|
1127
|
+
mockExecuteAppleScript
|
|
1128
|
+
// Check "Retro Tech" — exists
|
|
1129
|
+
.mockReturnValueOnce({ success: true, output: "x-coredata://A/ICFolder/p1" })
|
|
1130
|
+
// Check "Retro Tech/PC" — doesn't exist
|
|
1131
|
+
.mockReturnValueOnce({ success: false, output: "", error: "Can't get folder" })
|
|
1132
|
+
// Create "PC" inside "Retro Tech"
|
|
1133
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" })
|
|
1134
|
+
// Get ID of final folder
|
|
1135
|
+
.mockReturnValueOnce({ success: true, output: "folder id x-coredata://A/ICFolder/p2" });
|
|
1136
|
+
const result = manager.createFolder("Retro Tech/PC");
|
|
1137
|
+
expect(result).not.toBeNull();
|
|
1138
|
+
expect(result?.name).toBe("Retro Tech/PC");
|
|
1139
|
+
// No create call for "Retro Tech" — only for "PC"
|
|
1140
|
+
const createCalls = mockExecuteAppleScript.mock.calls.filter((c) => c[0].includes("make new folder"));
|
|
1141
|
+
expect(createCalls).toHaveLength(1);
|
|
1142
|
+
expect(createCalls[0][0]).toContain('name:"PC"');
|
|
1143
|
+
});
|
|
1068
1144
|
});
|
|
1069
1145
|
describe("deleteFolder", () => {
|
|
1070
1146
|
it("returns true on successful deletion", () => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Warnings for create-note / update-note
|
|
3
|
+
*
|
|
4
|
+
* Detects content patterns that look like the user is trying to do something
|
|
5
|
+
* Apple Notes via AppleScript cannot actually render — so the response can
|
|
6
|
+
* carry a clear warning instead of silently producing a broken note.
|
|
7
|
+
*
|
|
8
|
+
* @module utils/contentWarnings
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Detects checklist-like syntax in note content.
|
|
12
|
+
*
|
|
13
|
+
* Apple Notes checklists are a paragraph style stored in a protobuf blob in
|
|
14
|
+
* the NoteStore SQLite database. AppleScript's `body of note` setter does not
|
|
15
|
+
* expose paragraph styles: `<input type="checkbox">` is stripped, a
|
|
16
|
+
* `class="checklist"` on `<ul>` is dropped, and markdown `- [ ]` lines in
|
|
17
|
+
* `plaintext` mode arrive as literal text. There is no input that produces a
|
|
18
|
+
* real checklist.
|
|
19
|
+
*
|
|
20
|
+
* @param content - The user-supplied note body (HTML or plaintext)
|
|
21
|
+
* @returns A user-facing warning string, or null when no checklist-like
|
|
22
|
+
* patterns are present
|
|
23
|
+
*/
|
|
24
|
+
export function detectChecklistAttempt(content) {
|
|
25
|
+
if (!content)
|
|
26
|
+
return null;
|
|
27
|
+
// HTML checkbox input — `<input type="checkbox" ...>` in either quoting style.
|
|
28
|
+
const htmlCheckbox = /<input\b[^>]*\btype\s*=\s*["']checkbox["']/i.test(content);
|
|
29
|
+
// Markdown-style checklist: lines starting with optional whitespace, then
|
|
30
|
+
// `-` or `*`, a space, and `[ ]` / `[x]` / `[X]`.
|
|
31
|
+
const markdownCheckbox = /^[ \t]*[-*]\s+\[[ xX]\]/m.test(content);
|
|
32
|
+
// CSS class hint — some clients try `<ul class="checklist">` or
|
|
33
|
+
// `<li class="todo">`. AppleScript drops these classes too.
|
|
34
|
+
const checklistClass = /class\s*=\s*["'][^"']*\b(?:checklist|todo)\b/i.test(content);
|
|
35
|
+
if (!htmlCheckbox && !markdownCheckbox && !checklistClass)
|
|
36
|
+
return null;
|
|
37
|
+
return ("\n\n⚠️ Your content looks like a checklist, but Apple Notes checklists " +
|
|
38
|
+
'cannot be created via AppleScript — `<input type="checkbox">` is ' +
|
|
39
|
+
"stripped, checklist CSS classes are dropped, and markdown `- [ ]` lines " +
|
|
40
|
+
"arrive as literal text. The note was created with the surrounding " +
|
|
41
|
+
"structure (list items or paragraphs) intact. To convert it to a real " +
|
|
42
|
+
"Apple Notes checklist, open the note, select the items, and press " +
|
|
43
|
+
"⇧⌘L (Format → Checklist).");
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the content-warning detectors.
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { detectChecklistAttempt } from "./contentWarnings.js";
|
|
6
|
+
describe("detectChecklistAttempt", () => {
|
|
7
|
+
it("returns null for plain text without checklist syntax", () => {
|
|
8
|
+
expect(detectChecklistAttempt("Just a regular note.")).toBeNull();
|
|
9
|
+
});
|
|
10
|
+
it("returns null for empty content", () => {
|
|
11
|
+
expect(detectChecklistAttempt("")).toBeNull();
|
|
12
|
+
});
|
|
13
|
+
it("returns null for HTML lists that are not checklists", () => {
|
|
14
|
+
expect(detectChecklistAttempt("<ul><li>Apple</li><li>Banana</li></ul>")).toBeNull();
|
|
15
|
+
});
|
|
16
|
+
it('warns on <input type="checkbox"> (double-quoted)', () => {
|
|
17
|
+
const w = detectChecklistAttempt('<input type="checkbox"> Buy milk');
|
|
18
|
+
expect(w).not.toBeNull();
|
|
19
|
+
expect(w).toContain("⚠️");
|
|
20
|
+
expect(w).toContain("⇧⌘L");
|
|
21
|
+
});
|
|
22
|
+
it("warns on <input type='checkbox'> (single-quoted)", () => {
|
|
23
|
+
expect(detectChecklistAttempt("<input type='checkbox'> Buy milk")).not.toBeNull();
|
|
24
|
+
});
|
|
25
|
+
it("warns on <input> with extra attributes before type", () => {
|
|
26
|
+
expect(detectChecklistAttempt('<input id="x" type="checkbox"> Item')).not.toBeNull();
|
|
27
|
+
});
|
|
28
|
+
it('warns on <INPUT TYPE="CHECKBOX"> (case-insensitive)', () => {
|
|
29
|
+
expect(detectChecklistAttempt('<INPUT TYPE="CHECKBOX"> Item')).not.toBeNull();
|
|
30
|
+
});
|
|
31
|
+
it("warns on markdown `- [ ]` syntax", () => {
|
|
32
|
+
expect(detectChecklistAttempt("- [ ] todo 1\n- [x] done 1")).not.toBeNull();
|
|
33
|
+
});
|
|
34
|
+
it("warns on markdown `* [ ]` syntax", () => {
|
|
35
|
+
expect(detectChecklistAttempt("* [ ] todo 1")).not.toBeNull();
|
|
36
|
+
});
|
|
37
|
+
it("warns on markdown checklist with leading whitespace", () => {
|
|
38
|
+
expect(detectChecklistAttempt(" - [ ] indented todo")).not.toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('warns on <ul class="checklist">', () => {
|
|
41
|
+
expect(detectChecklistAttempt('<ul class="checklist"><li>a</li></ul>')).not.toBeNull();
|
|
42
|
+
});
|
|
43
|
+
it('warns on <li class="todo">', () => {
|
|
44
|
+
expect(detectChecklistAttempt('<ul><li class="todo">a</li></ul>')).not.toBeNull();
|
|
45
|
+
});
|
|
46
|
+
it("does not warn on the word 'checklist' in prose", () => {
|
|
47
|
+
expect(detectChecklistAttempt("My checklist of things to do tomorrow.")).toBeNull();
|
|
48
|
+
});
|
|
49
|
+
it("does not warn on a literal `[ ]` not at start of a list line", () => {
|
|
50
|
+
expect(detectChecklistAttempt("The brackets [ ] are not a checklist.")).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apple-notes-mcp",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "MCP server for Apple Notes - create, search, update, and manage notes via Claude",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"format": "prettier --write src",
|
|
26
26
|
"format:check": "prettier --check src",
|
|
27
27
|
"typecheck": "tsc --noEmit",
|
|
28
|
+
"version": "node -e \"const p=require('./package.json'); const f='.claude-plugin/plugin.json'; const c=JSON.parse(require('fs').readFileSync(f,'utf8')); c.version=p.version; require('fs').writeFileSync(f,JSON.stringify(c,null,2)+'\\n')\" && git add .claude-plugin/plugin.json",
|
|
28
29
|
"prepublishOnly": "npm run lint && npm run test && npm run build",
|
|
29
30
|
"prepare": "husky && npm run build"
|
|
30
31
|
},
|