apple-notes-mcp 1.4.4 → 2.0.0
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 +97 -6
- package/build/index.js +96 -42
- package/build/services/appleNotesManager.js +254 -140
- package/build/services/appleNotesManager.test.js +195 -67
- package/build/services/attachmentSave.test.js +85 -0
- package/build/services/fileConfig.js +51 -0
- package/build/services/fileConfig.test.js +48 -0
- package/build/tools/doctor.js +50 -0
- package/build/tools/doctor.test.js +42 -0
- package/build/tools/resourcesAndPrompts.js +70 -0
- package/build/tools/resourcesAndPrompts.test.js +63 -0
- package/build/utils/applescript.js +47 -3
- package/build/utils/applescript.test.js +29 -1
- package/build/utils/attachmentFs.js +59 -0
- package/build/utils/attachmentFs.test.js +46 -0
- package/build/utils/jxa.js +17 -0
- package/build/utils/jxa.test.js +20 -1
- package/package.json +1 -1
|
@@ -25,6 +25,11 @@ import { executeAppleScript } from "../utils/applescript.js";
|
|
|
25
25
|
const mockExecuteAppleScript = vi.mocked(executeAppleScript);
|
|
26
26
|
import { getChecklistItems } from "../utils/checklistParser.js";
|
|
27
27
|
const mockGetChecklistItems = vi.mocked(getChecklistItems);
|
|
28
|
+
// Result delimiters (#18) — must match appleNotesManager.ts.
|
|
29
|
+
// FIELD_SEP (US, \x1f) separates fields within a record;
|
|
30
|
+
// RECORD_SEP (RS, \x1e) separates records within a list.
|
|
31
|
+
const F = "\x1f";
|
|
32
|
+
const R = "\x1e";
|
|
28
33
|
// =============================================================================
|
|
29
34
|
// Text Escaping Tests
|
|
30
35
|
// =============================================================================
|
|
@@ -191,6 +196,23 @@ describe("parseAppleScriptDate", () => {
|
|
|
191
196
|
expect(evening.getHours()).toBe(21);
|
|
192
197
|
});
|
|
193
198
|
});
|
|
199
|
+
describe("locale-independent numeric format (#25)", () => {
|
|
200
|
+
it("parses the Y-M-D-H-m-s form emitted by our producers", () => {
|
|
201
|
+
const result = parseAppleScriptDate("2025-12-27-15-44-2");
|
|
202
|
+
expect(result.getFullYear()).toBe(2025);
|
|
203
|
+
expect(result.getMonth()).toBe(11);
|
|
204
|
+
expect(result.getDate()).toBe(27);
|
|
205
|
+
expect(result.getHours()).toBe(15);
|
|
206
|
+
expect(result.getMinutes()).toBe(44);
|
|
207
|
+
expect(result.getSeconds()).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
it("handles single-digit components and midnight", () => {
|
|
210
|
+
const result = parseAppleScriptDate("2025-1-5-0-0-0");
|
|
211
|
+
expect(result.getMonth()).toBe(0);
|
|
212
|
+
expect(result.getDate()).toBe(5);
|
|
213
|
+
expect(result.getHours()).toBe(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
194
216
|
describe("fallback behavior", () => {
|
|
195
217
|
it("returns current date for invalid input", () => {
|
|
196
218
|
const before = new Date();
|
|
@@ -437,7 +459,11 @@ describe("AppleNotesManager", () => {
|
|
|
437
459
|
it("returns array of matching notes with folder info", () => {
|
|
438
460
|
mockExecuteAppleScript.mockReturnValue({
|
|
439
461
|
success: true,
|
|
440
|
-
output:
|
|
462
|
+
output: [
|
|
463
|
+
["Meeting Notes", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
|
|
464
|
+
["Project Plan", "x-coredata://ABC/ICNote/p2", "Notes"].join(F),
|
|
465
|
+
["Weekly Review", "x-coredata://ABC/ICNote/p3", "Archive"].join(F),
|
|
466
|
+
].join(R),
|
|
441
467
|
});
|
|
442
468
|
const results = manager.searchNotes("notes");
|
|
443
469
|
expect(results).toHaveLength(3);
|
|
@@ -459,19 +485,18 @@ describe("AppleNotesManager", () => {
|
|
|
459
485
|
const results = manager.searchNotes("nonexistent");
|
|
460
486
|
expect(results).toHaveLength(0);
|
|
461
487
|
});
|
|
462
|
-
it("
|
|
488
|
+
it("throws on AppleScript error rather than returning empty (#19)", () => {
|
|
463
489
|
mockExecuteAppleScript.mockReturnValue({
|
|
464
490
|
success: false,
|
|
465
491
|
output: "",
|
|
466
492
|
error: "Search failed",
|
|
467
493
|
});
|
|
468
|
-
|
|
469
|
-
expect(results).toHaveLength(0);
|
|
494
|
+
expect(() => manager.searchNotes("test")).toThrow(/Search failed/);
|
|
470
495
|
});
|
|
471
496
|
it("searches content when searchContent is true", () => {
|
|
472
497
|
mockExecuteAppleScript.mockReturnValue({
|
|
473
498
|
success: true,
|
|
474
|
-
output: "Note with keyword
|
|
499
|
+
output: ["Note with keyword", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
475
500
|
});
|
|
476
501
|
manager.searchNotes("project alpha", true);
|
|
477
502
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('body contains "project alpha"'));
|
|
@@ -479,7 +504,7 @@ describe("AppleNotesManager", () => {
|
|
|
479
504
|
it("searches titles when searchContent is false", () => {
|
|
480
505
|
mockExecuteAppleScript.mockReturnValue({
|
|
481
506
|
success: true,
|
|
482
|
-
output: "Project Alpha Notes
|
|
507
|
+
output: ["Project Alpha Notes", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
483
508
|
});
|
|
484
509
|
manager.searchNotes("Project Alpha", false);
|
|
485
510
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('name contains "Project Alpha"'));
|
|
@@ -487,7 +512,10 @@ describe("AppleNotesManager", () => {
|
|
|
487
512
|
it("identifies notes in Recently Deleted folder", () => {
|
|
488
513
|
mockExecuteAppleScript.mockReturnValue({
|
|
489
514
|
success: true,
|
|
490
|
-
output:
|
|
515
|
+
output: [
|
|
516
|
+
["Old Note", "x-coredata://ABC/ICNote/p1", "Recently Deleted"].join(F),
|
|
517
|
+
["Active Note", "x-coredata://ABC/ICNote/p2", "Notes"].join(F),
|
|
518
|
+
].join(R),
|
|
491
519
|
});
|
|
492
520
|
const results = manager.searchNotes("note");
|
|
493
521
|
expect(results).toHaveLength(2);
|
|
@@ -509,7 +537,7 @@ describe("AppleNotesManager", () => {
|
|
|
509
537
|
it("limits search to specified folder", () => {
|
|
510
538
|
mockExecuteAppleScript.mockReturnValue({
|
|
511
539
|
success: true,
|
|
512
|
-
output: "Work Note
|
|
540
|
+
output: ["Work Note", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
|
|
513
541
|
});
|
|
514
542
|
manager.searchNotes("note", false, undefined, "Work");
|
|
515
543
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
|
|
@@ -527,7 +555,7 @@ describe("AppleNotesManager", () => {
|
|
|
527
555
|
it("adds date filter when modifiedSince is provided", () => {
|
|
528
556
|
mockExecuteAppleScript.mockReturnValue({
|
|
529
557
|
success: true,
|
|
530
|
-
output: "Recent Note
|
|
558
|
+
output: ["Recent Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
531
559
|
});
|
|
532
560
|
manager.searchNotes("note", false, undefined, undefined, "2025-06-15T00:00:00");
|
|
533
561
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -542,7 +570,7 @@ describe("AppleNotesManager", () => {
|
|
|
542
570
|
it("combines date filter with content search", () => {
|
|
543
571
|
mockExecuteAppleScript.mockReturnValue({
|
|
544
572
|
success: true,
|
|
545
|
-
output: "Note
|
|
573
|
+
output: ["Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
546
574
|
});
|
|
547
575
|
manager.searchNotes("keyword", true, undefined, undefined, "2025-01-01");
|
|
548
576
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -553,7 +581,7 @@ describe("AppleNotesManager", () => {
|
|
|
553
581
|
it("ignores invalid modifiedSince date", () => {
|
|
554
582
|
mockExecuteAppleScript.mockReturnValue({
|
|
555
583
|
success: true,
|
|
556
|
-
output: "Note
|
|
584
|
+
output: ["Note", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
557
585
|
});
|
|
558
586
|
manager.searchNotes("note", false, undefined, undefined, "not-a-date");
|
|
559
587
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -562,7 +590,7 @@ describe("AppleNotesManager", () => {
|
|
|
562
590
|
it("applies limit to search results", () => {
|
|
563
591
|
mockExecuteAppleScript.mockReturnValue({
|
|
564
592
|
success: true,
|
|
565
|
-
output: "Note 1
|
|
593
|
+
output: ["Note 1", "x-coredata://ABC/ICNote/p1", "Notes"].join(F),
|
|
566
594
|
});
|
|
567
595
|
manager.searchNotes("note", false, undefined, undefined, undefined, 5);
|
|
568
596
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -572,7 +600,7 @@ describe("AppleNotesManager", () => {
|
|
|
572
600
|
it("combines modifiedSince, limit, folder, and content search", () => {
|
|
573
601
|
mockExecuteAppleScript.mockReturnValue({
|
|
574
602
|
success: true,
|
|
575
|
-
output: "Note
|
|
603
|
+
output: ["Note", "x-coredata://ABC/ICNote/p1", "Work"].join(F),
|
|
576
604
|
});
|
|
577
605
|
manager.searchNotes("project", true, "iCloud", "Work", "2025-03-01", 10);
|
|
578
606
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -621,7 +649,14 @@ describe("AppleNotesManager", () => {
|
|
|
621
649
|
it("returns true when note is password-protected", () => {
|
|
622
650
|
mockExecuteAppleScript.mockReturnValue({
|
|
623
651
|
success: true,
|
|
624
|
-
output:
|
|
652
|
+
output: [
|
|
653
|
+
"Locked Note",
|
|
654
|
+
"x-coredata://ABC/ICNote/p1",
|
|
655
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
656
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
657
|
+
"false",
|
|
658
|
+
"true",
|
|
659
|
+
].join(F),
|
|
625
660
|
});
|
|
626
661
|
const result = manager.isNotePasswordProtected("Locked Note");
|
|
627
662
|
expect(result).toBe(true);
|
|
@@ -629,7 +664,14 @@ describe("AppleNotesManager", () => {
|
|
|
629
664
|
it("returns false when note is not password-protected", () => {
|
|
630
665
|
mockExecuteAppleScript.mockReturnValue({
|
|
631
666
|
success: true,
|
|
632
|
-
output:
|
|
667
|
+
output: [
|
|
668
|
+
"Open Note",
|
|
669
|
+
"x-coredata://ABC/ICNote/p2",
|
|
670
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
671
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
672
|
+
"false",
|
|
673
|
+
"false",
|
|
674
|
+
].join(F),
|
|
633
675
|
});
|
|
634
676
|
const result = manager.isNotePasswordProtected("Open Note");
|
|
635
677
|
expect(result).toBe(false);
|
|
@@ -648,7 +690,14 @@ describe("AppleNotesManager", () => {
|
|
|
648
690
|
it("returns true when note is password-protected", () => {
|
|
649
691
|
mockExecuteAppleScript.mockReturnValue({
|
|
650
692
|
success: true,
|
|
651
|
-
output:
|
|
693
|
+
output: [
|
|
694
|
+
"Locked Note",
|
|
695
|
+
"x-coredata://ABC/ICNote/p1",
|
|
696
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
697
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
698
|
+
"false",
|
|
699
|
+
"true",
|
|
700
|
+
].join(F),
|
|
652
701
|
});
|
|
653
702
|
const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p1");
|
|
654
703
|
expect(result).toBe(true);
|
|
@@ -656,7 +705,14 @@ describe("AppleNotesManager", () => {
|
|
|
656
705
|
it("returns false when note is not password-protected", () => {
|
|
657
706
|
mockExecuteAppleScript.mockReturnValue({
|
|
658
707
|
success: true,
|
|
659
|
-
output:
|
|
708
|
+
output: [
|
|
709
|
+
"Open Note",
|
|
710
|
+
"x-coredata://ABC/ICNote/p2",
|
|
711
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
712
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
713
|
+
"false",
|
|
714
|
+
"false",
|
|
715
|
+
].join(F),
|
|
660
716
|
});
|
|
661
717
|
const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p2");
|
|
662
718
|
expect(result).toBe(false);
|
|
@@ -678,7 +734,14 @@ describe("AppleNotesManager", () => {
|
|
|
678
734
|
it("returns Note object with metadata for valid ID", () => {
|
|
679
735
|
mockExecuteAppleScript.mockReturnValue({
|
|
680
736
|
success: true,
|
|
681
|
-
output:
|
|
737
|
+
output: [
|
|
738
|
+
"My Note",
|
|
739
|
+
"x-coredata://ABC123/ICNote/p100",
|
|
740
|
+
"Saturday, December 27, 2025 at 3:00:00 PM",
|
|
741
|
+
"Saturday, December 27, 2025 at 4:00:00 PM",
|
|
742
|
+
"false",
|
|
743
|
+
"false",
|
|
744
|
+
].join(F),
|
|
682
745
|
});
|
|
683
746
|
const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
|
|
684
747
|
expect(result).not.toBeNull();
|
|
@@ -716,7 +779,14 @@ describe("AppleNotesManager", () => {
|
|
|
716
779
|
it("correctly parses shared and passwordProtected as true", () => {
|
|
717
780
|
mockExecuteAppleScript.mockReturnValue({
|
|
718
781
|
success: true,
|
|
719
|
-
output:
|
|
782
|
+
output: [
|
|
783
|
+
"Shared Note",
|
|
784
|
+
"x-coredata://ABC/ICNote/p1",
|
|
785
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
786
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
787
|
+
"true",
|
|
788
|
+
"true",
|
|
789
|
+
].join(F),
|
|
720
790
|
});
|
|
721
791
|
const result = manager.getNoteById("x-coredata://ABC/ICNote/p1");
|
|
722
792
|
expect(result?.shared).toBe(true);
|
|
@@ -730,7 +800,14 @@ describe("AppleNotesManager", () => {
|
|
|
730
800
|
it("returns Note object with full metadata", () => {
|
|
731
801
|
mockExecuteAppleScript.mockReturnValue({
|
|
732
802
|
success: true,
|
|
733
|
-
output:
|
|
803
|
+
output: [
|
|
804
|
+
"Project Notes",
|
|
805
|
+
"x-coredata://ABC123/ICNote/p200",
|
|
806
|
+
"Friday, December 20, 2025 at 10:00:00 AM",
|
|
807
|
+
"Saturday, December 27, 2025 at 2:30:00 PM",
|
|
808
|
+
"false",
|
|
809
|
+
"false",
|
|
810
|
+
].join(F),
|
|
734
811
|
});
|
|
735
812
|
const result = manager.getNoteDetails("Project Notes");
|
|
736
813
|
expect(result).not.toBeNull();
|
|
@@ -750,7 +827,14 @@ describe("AppleNotesManager", () => {
|
|
|
750
827
|
it("uses specified account", () => {
|
|
751
828
|
mockExecuteAppleScript.mockReturnValue({
|
|
752
829
|
success: true,
|
|
753
|
-
output:
|
|
830
|
+
output: [
|
|
831
|
+
"Note",
|
|
832
|
+
"id123",
|
|
833
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
834
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
835
|
+
"false",
|
|
836
|
+
"false",
|
|
837
|
+
].join(F),
|
|
754
838
|
});
|
|
755
839
|
const result = manager.getNoteDetails("My Note", "Exchange");
|
|
756
840
|
expect(result?.account).toBe("Exchange");
|
|
@@ -759,7 +843,14 @@ describe("AppleNotesManager", () => {
|
|
|
759
843
|
it("handles shared notes correctly", () => {
|
|
760
844
|
mockExecuteAppleScript.mockReturnValue({
|
|
761
845
|
success: true,
|
|
762
|
-
output:
|
|
846
|
+
output: [
|
|
847
|
+
"Shared Doc",
|
|
848
|
+
"id456",
|
|
849
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
850
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
851
|
+
"true",
|
|
852
|
+
"false",
|
|
853
|
+
].join(F),
|
|
763
854
|
});
|
|
764
855
|
const result = manager.getNoteDetails("Shared Doc");
|
|
765
856
|
expect(result?.shared).toBe(true);
|
|
@@ -902,7 +993,7 @@ describe("AppleNotesManager", () => {
|
|
|
902
993
|
it("returns array of note titles", () => {
|
|
903
994
|
mockExecuteAppleScript.mockReturnValue({
|
|
904
995
|
success: true,
|
|
905
|
-
output: "Note A, Note B, Note C",
|
|
996
|
+
output: ["Note A", "Note B", "Note C"].join(R),
|
|
906
997
|
});
|
|
907
998
|
const titles = manager.listNotes();
|
|
908
999
|
expect(titles).toEqual(["Note A", "Note B", "Note C"]);
|
|
@@ -910,24 +1001,23 @@ describe("AppleNotesManager", () => {
|
|
|
910
1001
|
it("filters out empty entries", () => {
|
|
911
1002
|
mockExecuteAppleScript.mockReturnValue({
|
|
912
1003
|
success: true,
|
|
913
|
-
output: "Note A, , Note B, , ",
|
|
1004
|
+
output: ["Note A", "", "Note B", "", ""].join(R),
|
|
914
1005
|
});
|
|
915
1006
|
const titles = manager.listNotes();
|
|
916
1007
|
expect(titles).toEqual(["Note A", "Note B"]);
|
|
917
1008
|
});
|
|
918
|
-
it("
|
|
1009
|
+
it("throws on failure rather than returning empty (#19)", () => {
|
|
919
1010
|
mockExecuteAppleScript.mockReturnValue({
|
|
920
1011
|
success: false,
|
|
921
1012
|
output: "",
|
|
922
1013
|
error: "Account not found",
|
|
923
1014
|
});
|
|
924
|
-
|
|
925
|
-
expect(titles).toEqual([]);
|
|
1015
|
+
expect(() => manager.listNotes()).toThrow(/Account not found/);
|
|
926
1016
|
});
|
|
927
1017
|
it("filters by folder when specified", () => {
|
|
928
1018
|
mockExecuteAppleScript.mockReturnValue({
|
|
929
1019
|
success: true,
|
|
930
|
-
output: "Work Note 1, Work Note 2",
|
|
1020
|
+
output: ["Work Note 1", "Work Note 2"].join(R),
|
|
931
1021
|
});
|
|
932
1022
|
manager.listNotes("iCloud", "Work");
|
|
933
1023
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
|
|
@@ -935,7 +1025,7 @@ describe("AppleNotesManager", () => {
|
|
|
935
1025
|
it("uses whose clause when modifiedSince is provided", () => {
|
|
936
1026
|
mockExecuteAppleScript.mockReturnValue({
|
|
937
1027
|
success: true,
|
|
938
|
-
output: "Recent Note 1
|
|
1028
|
+
output: ["Recent Note 1", "Recent Note 2"].join(R),
|
|
939
1029
|
});
|
|
940
1030
|
const results = manager.listNotes(undefined, undefined, "2025-06-15T00:00:00");
|
|
941
1031
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -950,7 +1040,7 @@ describe("AppleNotesManager", () => {
|
|
|
950
1040
|
it("uses repeat loop when limit is provided", () => {
|
|
951
1041
|
mockExecuteAppleScript.mockReturnValue({
|
|
952
1042
|
success: true,
|
|
953
|
-
output: "Note 1
|
|
1043
|
+
output: ["Note 1", "Note 2", "Note 3"].join(R),
|
|
954
1044
|
});
|
|
955
1045
|
const results = manager.listNotes(undefined, undefined, undefined, 3);
|
|
956
1046
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -960,7 +1050,7 @@ describe("AppleNotesManager", () => {
|
|
|
960
1050
|
it("combines folder, modifiedSince, and limit", () => {
|
|
961
1051
|
mockExecuteAppleScript.mockReturnValue({
|
|
962
1052
|
success: true,
|
|
963
|
-
output: "Work Note
|
|
1053
|
+
output: ["Work Note", "Another Work Note"].join(R),
|
|
964
1054
|
});
|
|
965
1055
|
manager.listNotes("iCloud", "Work", "2025-01-01", 10);
|
|
966
1056
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -979,7 +1069,7 @@ describe("AppleNotesManager", () => {
|
|
|
979
1069
|
it("ignores invalid modifiedSince date and falls back to limit-only", () => {
|
|
980
1070
|
mockExecuteAppleScript.mockReturnValue({
|
|
981
1071
|
success: true,
|
|
982
|
-
output: "Note 1
|
|
1072
|
+
output: ["Note 1", "Note 2"].join(R),
|
|
983
1073
|
});
|
|
984
1074
|
const results = manager.listNotes(undefined, undefined, "not-a-date", 5);
|
|
985
1075
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -1170,7 +1260,14 @@ describe("AppleNotesManager", () => {
|
|
|
1170
1260
|
mockExecuteAppleScript
|
|
1171
1261
|
.mockReturnValueOnce({
|
|
1172
1262
|
success: true,
|
|
1173
|
-
output:
|
|
1263
|
+
output: [
|
|
1264
|
+
"My Note",
|
|
1265
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1266
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1267
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1268
|
+
"false",
|
|
1269
|
+
"false",
|
|
1270
|
+
].join(F),
|
|
1174
1271
|
})
|
|
1175
1272
|
.mockReturnValueOnce({
|
|
1176
1273
|
success: true,
|
|
@@ -1202,7 +1299,14 @@ describe("AppleNotesManager", () => {
|
|
|
1202
1299
|
mockExecuteAppleScript
|
|
1203
1300
|
.mockReturnValueOnce({
|
|
1204
1301
|
success: true,
|
|
1205
|
-
output:
|
|
1302
|
+
output: [
|
|
1303
|
+
"My Note",
|
|
1304
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1305
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1306
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1307
|
+
"false",
|
|
1308
|
+
"false",
|
|
1309
|
+
].join(F),
|
|
1206
1310
|
})
|
|
1207
1311
|
.mockReturnValueOnce({
|
|
1208
1312
|
success: true,
|
|
@@ -1222,7 +1326,14 @@ describe("AppleNotesManager", () => {
|
|
|
1222
1326
|
mockExecuteAppleScript
|
|
1223
1327
|
.mockReturnValueOnce({
|
|
1224
1328
|
success: true,
|
|
1225
|
-
output:
|
|
1329
|
+
output: [
|
|
1330
|
+
"My Note",
|
|
1331
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1332
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1333
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1334
|
+
"false",
|
|
1335
|
+
"false",
|
|
1336
|
+
].join(F),
|
|
1226
1337
|
})
|
|
1227
1338
|
.mockReturnValueOnce({
|
|
1228
1339
|
success: true,
|
|
@@ -1249,7 +1360,7 @@ describe("AppleNotesManager", () => {
|
|
|
1249
1360
|
it("returns array of Account objects", () => {
|
|
1250
1361
|
mockExecuteAppleScript.mockReturnValue({
|
|
1251
1362
|
success: true,
|
|
1252
|
-
output: "iCloud, Gmail, Exchange",
|
|
1363
|
+
output: ["iCloud", "Gmail", "Exchange"].join(R),
|
|
1253
1364
|
});
|
|
1254
1365
|
const accounts = manager.listAccounts();
|
|
1255
1366
|
expect(accounts).toHaveLength(3);
|
|
@@ -1257,14 +1368,13 @@ describe("AppleNotesManager", () => {
|
|
|
1257
1368
|
expect(accounts[1].name).toBe("Gmail");
|
|
1258
1369
|
expect(accounts[2].name).toBe("Exchange");
|
|
1259
1370
|
});
|
|
1260
|
-
it("
|
|
1371
|
+
it("throws on failure rather than returning empty (#19)", () => {
|
|
1261
1372
|
mockExecuteAppleScript.mockReturnValue({
|
|
1262
1373
|
success: false,
|
|
1263
1374
|
output: "",
|
|
1264
1375
|
error: "Notes.app not available",
|
|
1265
1376
|
});
|
|
1266
|
-
|
|
1267
|
-
expect(accounts).toEqual([]);
|
|
1377
|
+
expect(() => manager.listAccounts()).toThrow(/Notes.app not available/);
|
|
1268
1378
|
});
|
|
1269
1379
|
});
|
|
1270
1380
|
// ---------------------------------------------------------------------------
|
|
@@ -1280,7 +1390,7 @@ describe("AppleNotesManager", () => {
|
|
|
1280
1390
|
// Check 3: listAccounts
|
|
1281
1391
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1282
1392
|
// Check 4: listNotes
|
|
1283
|
-
.mockReturnValueOnce({ success: true, output: "Note 1, Note 2" });
|
|
1393
|
+
.mockReturnValueOnce({ success: true, output: ["Note 1", "Note 2"].join(R) });
|
|
1284
1394
|
const result = manager.healthCheck();
|
|
1285
1395
|
expect(result.healthy).toBe(true);
|
|
1286
1396
|
expect(result.checks).toHaveLength(4);
|
|
@@ -1321,7 +1431,7 @@ describe("AppleNotesManager", () => {
|
|
|
1321
1431
|
mockExecuteAppleScript
|
|
1322
1432
|
.mockReturnValueOnce({ success: true, output: "ok" })
|
|
1323
1433
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1324
|
-
.mockReturnValueOnce({ success: true, output: "iCloud, Gmail" })
|
|
1434
|
+
.mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
|
|
1325
1435
|
.mockReturnValueOnce({ success: true, output: "" });
|
|
1326
1436
|
const result = manager.healthCheck();
|
|
1327
1437
|
const accountCheck = result.checks.find((c) => c.name === "accounts");
|
|
@@ -1337,14 +1447,13 @@ describe("AppleNotesManager", () => {
|
|
|
1337
1447
|
mockExecuteAppleScript
|
|
1338
1448
|
// listAccounts
|
|
1339
1449
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1340
|
-
//
|
|
1341
|
-
.mockReturnValueOnce({
|
|
1342
|
-
|
|
1343
|
-
.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1450
|
+
// per-account folder counts: name<F>count, records joined by R
|
|
1451
|
+
.mockReturnValueOnce({
|
|
1452
|
+
success: true,
|
|
1453
|
+
output: ["Notes", "3"].join(F) + R + ["Work", "2"].join(F) + R,
|
|
1454
|
+
})
|
|
1455
|
+
// getRecentlyModifiedCounts: c1<F>c7<F>c30
|
|
1456
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1348
1457
|
const stats = manager.getNotesStats();
|
|
1349
1458
|
expect(stats.totalNotes).toBe(5);
|
|
1350
1459
|
expect(stats.accounts).toHaveLength(1);
|
|
@@ -1356,9 +1465,8 @@ describe("AppleNotesManager", () => {
|
|
|
1356
1465
|
it("returns zero counts when no notes exist", () => {
|
|
1357
1466
|
mockExecuteAppleScript
|
|
1358
1467
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1359
|
-
.mockReturnValueOnce({ success: true, output: "
|
|
1360
|
-
.mockReturnValueOnce({ success: true, output: "" })
|
|
1361
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1468
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "0"].join(F) + R })
|
|
1469
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1362
1470
|
const stats = manager.getNotesStats();
|
|
1363
1471
|
expect(stats.totalNotes).toBe(0);
|
|
1364
1472
|
expect(stats.recentlyModified.last24h).toBe(0);
|
|
@@ -1368,17 +1476,13 @@ describe("AppleNotesManager", () => {
|
|
|
1368
1476
|
it("handles multiple accounts", () => {
|
|
1369
1477
|
mockExecuteAppleScript
|
|
1370
1478
|
// listAccounts
|
|
1371
|
-
.mockReturnValueOnce({ success: true, output: "iCloud, Gmail" })
|
|
1372
|
-
//
|
|
1373
|
-
.mockReturnValueOnce({ success: true, output: "
|
|
1374
|
-
//
|
|
1375
|
-
.mockReturnValueOnce({ success: true, output: "
|
|
1376
|
-
// listFolders for Gmail
|
|
1377
|
-
.mockReturnValueOnce({ success: true, output: "id2\tNotes" })
|
|
1378
|
-
// listNotes for Gmail/Notes
|
|
1379
|
-
.mockReturnValueOnce({ success: true, output: "Email Note" })
|
|
1479
|
+
.mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
|
|
1480
|
+
// iCloud folder counts
|
|
1481
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
|
|
1482
|
+
// Gmail folder counts
|
|
1483
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
|
|
1380
1484
|
// getRecentlyModifiedCounts
|
|
1381
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1485
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1382
1486
|
const stats = manager.getNotesStats();
|
|
1383
1487
|
expect(stats.totalNotes).toBe(2);
|
|
1384
1488
|
expect(stats.accounts).toHaveLength(2);
|
|
@@ -1393,7 +1497,10 @@ describe("AppleNotesManager", () => {
|
|
|
1393
1497
|
it("returns attachments for a note", () => {
|
|
1394
1498
|
mockExecuteAppleScript.mockReturnValueOnce({
|
|
1395
1499
|
success: true,
|
|
1396
|
-
output:
|
|
1500
|
+
output: [
|
|
1501
|
+
["x-coredata://ABC/ICAttachment/p1", "photo.jpg", "public.jpeg"].join(F),
|
|
1502
|
+
["x-coredata://ABC/ICAttachment/p2", "document.pdf", "com.adobe.pdf"].join(F),
|
|
1503
|
+
].join(R),
|
|
1397
1504
|
});
|
|
1398
1505
|
const attachments = manager.listAttachmentsById("x-coredata://ABC/ICNote/p123");
|
|
1399
1506
|
expect(attachments).toHaveLength(2);
|
|
@@ -1432,7 +1539,7 @@ describe("AppleNotesManager", () => {
|
|
|
1432
1539
|
it("returns attachments for a note by title", () => {
|
|
1433
1540
|
mockExecuteAppleScript.mockReturnValueOnce({
|
|
1434
1541
|
success: true,
|
|
1435
|
-
output: "attach-id
|
|
1542
|
+
output: ["attach-id", "image.png", "public.png"].join(F),
|
|
1436
1543
|
});
|
|
1437
1544
|
const attachments = manager.listAttachments("My Note");
|
|
1438
1545
|
expect(attachments).toHaveLength(1);
|
|
@@ -1463,7 +1570,14 @@ describe("AppleNotesManager", () => {
|
|
|
1463
1570
|
// ---------------------------------------------------------------------------
|
|
1464
1571
|
describe("batchDeleteNotes", () => {
|
|
1465
1572
|
// Helper to create getNoteById mock output (matches AppleScript format)
|
|
1466
|
-
const noteByIdOutput = (title, passwordProtected = false) =>
|
|
1573
|
+
const noteByIdOutput = (title, passwordProtected = false) => [
|
|
1574
|
+
title,
|
|
1575
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1576
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1577
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1578
|
+
"false",
|
|
1579
|
+
String(passwordProtected),
|
|
1580
|
+
].join(F);
|
|
1467
1581
|
it("deletes multiple notes successfully", () => {
|
|
1468
1582
|
// For each note: getNoteById (which isNotePasswordProtectedById also calls), deleteNoteById
|
|
1469
1583
|
mockExecuteAppleScript
|
|
@@ -1548,7 +1662,14 @@ describe("AppleNotesManager", () => {
|
|
|
1548
1662
|
});
|
|
1549
1663
|
describe("batchMoveNotes", () => {
|
|
1550
1664
|
// Helper to create getNoteById mock output (matches AppleScript format)
|
|
1551
|
-
const noteByIdOutput = (title, passwordProtected = false) =>
|
|
1665
|
+
const noteByIdOutput = (title, passwordProtected = false) => [
|
|
1666
|
+
title,
|
|
1667
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1668
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1669
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1670
|
+
"false",
|
|
1671
|
+
String(passwordProtected),
|
|
1672
|
+
].join(F);
|
|
1552
1673
|
it("moves multiple notes successfully", () => {
|
|
1553
1674
|
// For each note: getNoteById, getNoteById (password check), getNoteContentById, create, delete
|
|
1554
1675
|
mockExecuteAppleScript
|
|
@@ -1619,7 +1740,14 @@ describe("AppleNotesManager", () => {
|
|
|
1619
1740
|
// ---------------------------------------------------------------------------
|
|
1620
1741
|
describe("exportNotesAsJson", () => {
|
|
1621
1742
|
// Note details output helper - format: title, id, date, date, shared, passwordProtected
|
|
1622
|
-
const noteDetailsOutput = (title, passwordProtected = false) =>
|
|
1743
|
+
const noteDetailsOutput = (title, passwordProtected = false) => [
|
|
1744
|
+
title,
|
|
1745
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1746
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1747
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1748
|
+
"false",
|
|
1749
|
+
String(passwordProtected),
|
|
1750
|
+
].join(F);
|
|
1623
1751
|
it("exports notes with metadata and content", () => {
|
|
1624
1752
|
mockExecuteAppleScript
|
|
1625
1753
|
// listAccounts
|