apple-notes-mcp 1.4.0 → 1.4.1
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
|
@@ -812,7 +812,7 @@ The `\\\\` in JSON becomes `\\` in the actual string, which represents a single
|
|
|
812
812
|
```bash
|
|
813
813
|
npm install # Install dependencies
|
|
814
814
|
npm run build # Compile TypeScript
|
|
815
|
-
npm test # Run test suite (
|
|
815
|
+
npm test # Run test suite (313 tests)
|
|
816
816
|
npm run lint # Check code style
|
|
817
817
|
npm run format # Format code
|
|
818
818
|
```
|
package/build/index.js
CHANGED
|
@@ -454,10 +454,16 @@ server.tool("list-folders", {
|
|
|
454
454
|
return successResponse(`Found ${folders.length} folders${acct}:\n${folderList}${syncNote}`);
|
|
455
455
|
}, "Error listing folders"));
|
|
456
456
|
// --- create-folder ---
|
|
457
|
-
server.tool("create-folder",
|
|
457
|
+
server.tool("create-folder", {
|
|
458
|
+
name: z
|
|
459
|
+
.string()
|
|
460
|
+
.min(1, "Folder name is required")
|
|
461
|
+
.describe('Folder name or nested path separated by "/". E.g., "Retro Tech/PC/CPUs" creates all intermediate folders. Existing segments are skipped.'),
|
|
462
|
+
account: z.string().optional().describe("Account name (defaults to iCloud)"),
|
|
463
|
+
}, withErrorHandling(({ name, account }) => {
|
|
458
464
|
const folder = notesManager.createFolder(name, account);
|
|
459
465
|
if (!folder) {
|
|
460
|
-
return errorResponse(`Failed to create folder "${name}"
|
|
466
|
+
return errorResponse(`Failed to create folder "${name}".`);
|
|
461
467
|
}
|
|
462
468
|
return successResponse(`Folder created: "${folder.name}"`);
|
|
463
469
|
}, "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", () => {
|