apple-notes-mcp 1.4.4 → 2.0.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 +97 -6
- package/build/index.js +96 -42
- package/build/services/appleNotesManager.js +261 -147
- package/build/services/appleNotesManager.test.js +204 -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];
|
|
@@ -605,6 +633,15 @@ describe("AppleNotesManager", () => {
|
|
|
605
633
|
const content = manager.getNoteContent("Missing Note");
|
|
606
634
|
expect(content).toBe("");
|
|
607
635
|
});
|
|
636
|
+
it("looks up titles containing & literally, not HTML-escaped (regression)", () => {
|
|
637
|
+
// Bug found in live testing: titles with "&" were HTML-escaped to "&"
|
|
638
|
+
// in the `note "..."` lookup, so the note could never be found.
|
|
639
|
+
mockExecuteAppleScript.mockReturnValue({ success: true, output: "<div>x</div>" });
|
|
640
|
+
manager.getNoteContent("Tom & Jerry", "iCloud");
|
|
641
|
+
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
642
|
+
expect(script).toContain("Tom & Jerry");
|
|
643
|
+
expect(script).not.toContain("Tom & Jerry");
|
|
644
|
+
});
|
|
608
645
|
it("uses specified account", () => {
|
|
609
646
|
mockExecuteAppleScript.mockReturnValue({
|
|
610
647
|
success: true,
|
|
@@ -621,7 +658,14 @@ describe("AppleNotesManager", () => {
|
|
|
621
658
|
it("returns true when note is password-protected", () => {
|
|
622
659
|
mockExecuteAppleScript.mockReturnValue({
|
|
623
660
|
success: true,
|
|
624
|
-
output:
|
|
661
|
+
output: [
|
|
662
|
+
"Locked Note",
|
|
663
|
+
"x-coredata://ABC/ICNote/p1",
|
|
664
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
665
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
666
|
+
"false",
|
|
667
|
+
"true",
|
|
668
|
+
].join(F),
|
|
625
669
|
});
|
|
626
670
|
const result = manager.isNotePasswordProtected("Locked Note");
|
|
627
671
|
expect(result).toBe(true);
|
|
@@ -629,7 +673,14 @@ describe("AppleNotesManager", () => {
|
|
|
629
673
|
it("returns false when note is not password-protected", () => {
|
|
630
674
|
mockExecuteAppleScript.mockReturnValue({
|
|
631
675
|
success: true,
|
|
632
|
-
output:
|
|
676
|
+
output: [
|
|
677
|
+
"Open Note",
|
|
678
|
+
"x-coredata://ABC/ICNote/p2",
|
|
679
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
680
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
681
|
+
"false",
|
|
682
|
+
"false",
|
|
683
|
+
].join(F),
|
|
633
684
|
});
|
|
634
685
|
const result = manager.isNotePasswordProtected("Open Note");
|
|
635
686
|
expect(result).toBe(false);
|
|
@@ -648,7 +699,14 @@ describe("AppleNotesManager", () => {
|
|
|
648
699
|
it("returns true when note is password-protected", () => {
|
|
649
700
|
mockExecuteAppleScript.mockReturnValue({
|
|
650
701
|
success: true,
|
|
651
|
-
output:
|
|
702
|
+
output: [
|
|
703
|
+
"Locked Note",
|
|
704
|
+
"x-coredata://ABC/ICNote/p1",
|
|
705
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
706
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
707
|
+
"false",
|
|
708
|
+
"true",
|
|
709
|
+
].join(F),
|
|
652
710
|
});
|
|
653
711
|
const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p1");
|
|
654
712
|
expect(result).toBe(true);
|
|
@@ -656,7 +714,14 @@ describe("AppleNotesManager", () => {
|
|
|
656
714
|
it("returns false when note is not password-protected", () => {
|
|
657
715
|
mockExecuteAppleScript.mockReturnValue({
|
|
658
716
|
success: true,
|
|
659
|
-
output:
|
|
717
|
+
output: [
|
|
718
|
+
"Open Note",
|
|
719
|
+
"x-coredata://ABC/ICNote/p2",
|
|
720
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
721
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
722
|
+
"false",
|
|
723
|
+
"false",
|
|
724
|
+
].join(F),
|
|
660
725
|
});
|
|
661
726
|
const result = manager.isNotePasswordProtectedById("x-coredata://ABC/ICNote/p2");
|
|
662
727
|
expect(result).toBe(false);
|
|
@@ -678,7 +743,14 @@ describe("AppleNotesManager", () => {
|
|
|
678
743
|
it("returns Note object with metadata for valid ID", () => {
|
|
679
744
|
mockExecuteAppleScript.mockReturnValue({
|
|
680
745
|
success: true,
|
|
681
|
-
output:
|
|
746
|
+
output: [
|
|
747
|
+
"My Note",
|
|
748
|
+
"x-coredata://ABC123/ICNote/p100",
|
|
749
|
+
"Saturday, December 27, 2025 at 3:00:00 PM",
|
|
750
|
+
"Saturday, December 27, 2025 at 4:00:00 PM",
|
|
751
|
+
"false",
|
|
752
|
+
"false",
|
|
753
|
+
].join(F),
|
|
682
754
|
});
|
|
683
755
|
const result = manager.getNoteById("x-coredata://ABC123/ICNote/p100");
|
|
684
756
|
expect(result).not.toBeNull();
|
|
@@ -716,7 +788,14 @@ describe("AppleNotesManager", () => {
|
|
|
716
788
|
it("correctly parses shared and passwordProtected as true", () => {
|
|
717
789
|
mockExecuteAppleScript.mockReturnValue({
|
|
718
790
|
success: true,
|
|
719
|
-
output:
|
|
791
|
+
output: [
|
|
792
|
+
"Shared Note",
|
|
793
|
+
"x-coredata://ABC/ICNote/p1",
|
|
794
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
795
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
796
|
+
"true",
|
|
797
|
+
"true",
|
|
798
|
+
].join(F),
|
|
720
799
|
});
|
|
721
800
|
const result = manager.getNoteById("x-coredata://ABC/ICNote/p1");
|
|
722
801
|
expect(result?.shared).toBe(true);
|
|
@@ -730,7 +809,14 @@ describe("AppleNotesManager", () => {
|
|
|
730
809
|
it("returns Note object with full metadata", () => {
|
|
731
810
|
mockExecuteAppleScript.mockReturnValue({
|
|
732
811
|
success: true,
|
|
733
|
-
output:
|
|
812
|
+
output: [
|
|
813
|
+
"Project Notes",
|
|
814
|
+
"x-coredata://ABC123/ICNote/p200",
|
|
815
|
+
"Friday, December 20, 2025 at 10:00:00 AM",
|
|
816
|
+
"Saturday, December 27, 2025 at 2:30:00 PM",
|
|
817
|
+
"false",
|
|
818
|
+
"false",
|
|
819
|
+
].join(F),
|
|
734
820
|
});
|
|
735
821
|
const result = manager.getNoteDetails("Project Notes");
|
|
736
822
|
expect(result).not.toBeNull();
|
|
@@ -750,7 +836,14 @@ describe("AppleNotesManager", () => {
|
|
|
750
836
|
it("uses specified account", () => {
|
|
751
837
|
mockExecuteAppleScript.mockReturnValue({
|
|
752
838
|
success: true,
|
|
753
|
-
output:
|
|
839
|
+
output: [
|
|
840
|
+
"Note",
|
|
841
|
+
"id123",
|
|
842
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
843
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
844
|
+
"false",
|
|
845
|
+
"false",
|
|
846
|
+
].join(F),
|
|
754
847
|
});
|
|
755
848
|
const result = manager.getNoteDetails("My Note", "Exchange");
|
|
756
849
|
expect(result?.account).toBe("Exchange");
|
|
@@ -759,7 +852,14 @@ describe("AppleNotesManager", () => {
|
|
|
759
852
|
it("handles shared notes correctly", () => {
|
|
760
853
|
mockExecuteAppleScript.mockReturnValue({
|
|
761
854
|
success: true,
|
|
762
|
-
output:
|
|
855
|
+
output: [
|
|
856
|
+
"Shared Doc",
|
|
857
|
+
"id456",
|
|
858
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
859
|
+
"Monday, January 1, 2025 at 12:00:00 PM",
|
|
860
|
+
"true",
|
|
861
|
+
"false",
|
|
862
|
+
].join(F),
|
|
763
863
|
});
|
|
764
864
|
const result = manager.getNoteDetails("Shared Doc");
|
|
765
865
|
expect(result?.shared).toBe(true);
|
|
@@ -902,7 +1002,7 @@ describe("AppleNotesManager", () => {
|
|
|
902
1002
|
it("returns array of note titles", () => {
|
|
903
1003
|
mockExecuteAppleScript.mockReturnValue({
|
|
904
1004
|
success: true,
|
|
905
|
-
output: "Note A, Note B, Note C",
|
|
1005
|
+
output: ["Note A", "Note B", "Note C"].join(R),
|
|
906
1006
|
});
|
|
907
1007
|
const titles = manager.listNotes();
|
|
908
1008
|
expect(titles).toEqual(["Note A", "Note B", "Note C"]);
|
|
@@ -910,24 +1010,23 @@ describe("AppleNotesManager", () => {
|
|
|
910
1010
|
it("filters out empty entries", () => {
|
|
911
1011
|
mockExecuteAppleScript.mockReturnValue({
|
|
912
1012
|
success: true,
|
|
913
|
-
output: "Note A, , Note B, , ",
|
|
1013
|
+
output: ["Note A", "", "Note B", "", ""].join(R),
|
|
914
1014
|
});
|
|
915
1015
|
const titles = manager.listNotes();
|
|
916
1016
|
expect(titles).toEqual(["Note A", "Note B"]);
|
|
917
1017
|
});
|
|
918
|
-
it("
|
|
1018
|
+
it("throws on failure rather than returning empty (#19)", () => {
|
|
919
1019
|
mockExecuteAppleScript.mockReturnValue({
|
|
920
1020
|
success: false,
|
|
921
1021
|
output: "",
|
|
922
1022
|
error: "Account not found",
|
|
923
1023
|
});
|
|
924
|
-
|
|
925
|
-
expect(titles).toEqual([]);
|
|
1024
|
+
expect(() => manager.listNotes()).toThrow(/Account not found/);
|
|
926
1025
|
});
|
|
927
1026
|
it("filters by folder when specified", () => {
|
|
928
1027
|
mockExecuteAppleScript.mockReturnValue({
|
|
929
1028
|
success: true,
|
|
930
|
-
output: "Work Note 1, Work Note 2",
|
|
1029
|
+
output: ["Work Note 1", "Work Note 2"].join(R),
|
|
931
1030
|
});
|
|
932
1031
|
manager.listNotes("iCloud", "Work");
|
|
933
1032
|
expect(mockExecuteAppleScript).toHaveBeenCalledWith(expect.stringContaining('notes of folder "Work"'));
|
|
@@ -935,7 +1034,7 @@ describe("AppleNotesManager", () => {
|
|
|
935
1034
|
it("uses whose clause when modifiedSince is provided", () => {
|
|
936
1035
|
mockExecuteAppleScript.mockReturnValue({
|
|
937
1036
|
success: true,
|
|
938
|
-
output: "Recent Note 1
|
|
1037
|
+
output: ["Recent Note 1", "Recent Note 2"].join(R),
|
|
939
1038
|
});
|
|
940
1039
|
const results = manager.listNotes(undefined, undefined, "2025-06-15T00:00:00");
|
|
941
1040
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -950,7 +1049,7 @@ describe("AppleNotesManager", () => {
|
|
|
950
1049
|
it("uses repeat loop when limit is provided", () => {
|
|
951
1050
|
mockExecuteAppleScript.mockReturnValue({
|
|
952
1051
|
success: true,
|
|
953
|
-
output: "Note 1
|
|
1052
|
+
output: ["Note 1", "Note 2", "Note 3"].join(R),
|
|
954
1053
|
});
|
|
955
1054
|
const results = manager.listNotes(undefined, undefined, undefined, 3);
|
|
956
1055
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -960,7 +1059,7 @@ describe("AppleNotesManager", () => {
|
|
|
960
1059
|
it("combines folder, modifiedSince, and limit", () => {
|
|
961
1060
|
mockExecuteAppleScript.mockReturnValue({
|
|
962
1061
|
success: true,
|
|
963
|
-
output: "Work Note
|
|
1062
|
+
output: ["Work Note", "Another Work Note"].join(R),
|
|
964
1063
|
});
|
|
965
1064
|
manager.listNotes("iCloud", "Work", "2025-01-01", 10);
|
|
966
1065
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -979,7 +1078,7 @@ describe("AppleNotesManager", () => {
|
|
|
979
1078
|
it("ignores invalid modifiedSince date and falls back to limit-only", () => {
|
|
980
1079
|
mockExecuteAppleScript.mockReturnValue({
|
|
981
1080
|
success: true,
|
|
982
|
-
output: "Note 1
|
|
1081
|
+
output: ["Note 1", "Note 2"].join(R),
|
|
983
1082
|
});
|
|
984
1083
|
const results = manager.listNotes(undefined, undefined, "not-a-date", 5);
|
|
985
1084
|
const script = mockExecuteAppleScript.mock.calls[0][0];
|
|
@@ -1170,7 +1269,14 @@ describe("AppleNotesManager", () => {
|
|
|
1170
1269
|
mockExecuteAppleScript
|
|
1171
1270
|
.mockReturnValueOnce({
|
|
1172
1271
|
success: true,
|
|
1173
|
-
output:
|
|
1272
|
+
output: [
|
|
1273
|
+
"My Note",
|
|
1274
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1275
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1276
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1277
|
+
"false",
|
|
1278
|
+
"false",
|
|
1279
|
+
].join(F),
|
|
1174
1280
|
})
|
|
1175
1281
|
.mockReturnValueOnce({
|
|
1176
1282
|
success: true,
|
|
@@ -1202,7 +1308,14 @@ describe("AppleNotesManager", () => {
|
|
|
1202
1308
|
mockExecuteAppleScript
|
|
1203
1309
|
.mockReturnValueOnce({
|
|
1204
1310
|
success: true,
|
|
1205
|
-
output:
|
|
1311
|
+
output: [
|
|
1312
|
+
"My Note",
|
|
1313
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1314
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1315
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1316
|
+
"false",
|
|
1317
|
+
"false",
|
|
1318
|
+
].join(F),
|
|
1206
1319
|
})
|
|
1207
1320
|
.mockReturnValueOnce({
|
|
1208
1321
|
success: true,
|
|
@@ -1222,7 +1335,14 @@ describe("AppleNotesManager", () => {
|
|
|
1222
1335
|
mockExecuteAppleScript
|
|
1223
1336
|
.mockReturnValueOnce({
|
|
1224
1337
|
success: true,
|
|
1225
|
-
output:
|
|
1338
|
+
output: [
|
|
1339
|
+
"My Note",
|
|
1340
|
+
"x-coredata://ABC/ICNote/p123",
|
|
1341
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1342
|
+
"Monday, January 1, 2024 at 12:00:00 PM",
|
|
1343
|
+
"false",
|
|
1344
|
+
"false",
|
|
1345
|
+
].join(F),
|
|
1226
1346
|
})
|
|
1227
1347
|
.mockReturnValueOnce({
|
|
1228
1348
|
success: true,
|
|
@@ -1249,7 +1369,7 @@ describe("AppleNotesManager", () => {
|
|
|
1249
1369
|
it("returns array of Account objects", () => {
|
|
1250
1370
|
mockExecuteAppleScript.mockReturnValue({
|
|
1251
1371
|
success: true,
|
|
1252
|
-
output: "iCloud, Gmail, Exchange",
|
|
1372
|
+
output: ["iCloud", "Gmail", "Exchange"].join(R),
|
|
1253
1373
|
});
|
|
1254
1374
|
const accounts = manager.listAccounts();
|
|
1255
1375
|
expect(accounts).toHaveLength(3);
|
|
@@ -1257,14 +1377,13 @@ describe("AppleNotesManager", () => {
|
|
|
1257
1377
|
expect(accounts[1].name).toBe("Gmail");
|
|
1258
1378
|
expect(accounts[2].name).toBe("Exchange");
|
|
1259
1379
|
});
|
|
1260
|
-
it("
|
|
1380
|
+
it("throws on failure rather than returning empty (#19)", () => {
|
|
1261
1381
|
mockExecuteAppleScript.mockReturnValue({
|
|
1262
1382
|
success: false,
|
|
1263
1383
|
output: "",
|
|
1264
1384
|
error: "Notes.app not available",
|
|
1265
1385
|
});
|
|
1266
|
-
|
|
1267
|
-
expect(accounts).toEqual([]);
|
|
1386
|
+
expect(() => manager.listAccounts()).toThrow(/Notes.app not available/);
|
|
1268
1387
|
});
|
|
1269
1388
|
});
|
|
1270
1389
|
// ---------------------------------------------------------------------------
|
|
@@ -1280,7 +1399,7 @@ describe("AppleNotesManager", () => {
|
|
|
1280
1399
|
// Check 3: listAccounts
|
|
1281
1400
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1282
1401
|
// Check 4: listNotes
|
|
1283
|
-
.mockReturnValueOnce({ success: true, output: "Note 1, Note 2" });
|
|
1402
|
+
.mockReturnValueOnce({ success: true, output: ["Note 1", "Note 2"].join(R) });
|
|
1284
1403
|
const result = manager.healthCheck();
|
|
1285
1404
|
expect(result.healthy).toBe(true);
|
|
1286
1405
|
expect(result.checks).toHaveLength(4);
|
|
@@ -1321,7 +1440,7 @@ describe("AppleNotesManager", () => {
|
|
|
1321
1440
|
mockExecuteAppleScript
|
|
1322
1441
|
.mockReturnValueOnce({ success: true, output: "ok" })
|
|
1323
1442
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1324
|
-
.mockReturnValueOnce({ success: true, output: "iCloud, Gmail" })
|
|
1443
|
+
.mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
|
|
1325
1444
|
.mockReturnValueOnce({ success: true, output: "" });
|
|
1326
1445
|
const result = manager.healthCheck();
|
|
1327
1446
|
const accountCheck = result.checks.find((c) => c.name === "accounts");
|
|
@@ -1337,14 +1456,13 @@ describe("AppleNotesManager", () => {
|
|
|
1337
1456
|
mockExecuteAppleScript
|
|
1338
1457
|
// listAccounts
|
|
1339
1458
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1340
|
-
//
|
|
1341
|
-
.mockReturnValueOnce({
|
|
1342
|
-
|
|
1343
|
-
.
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1459
|
+
// per-account folder counts: name<F>count, records joined by R
|
|
1460
|
+
.mockReturnValueOnce({
|
|
1461
|
+
success: true,
|
|
1462
|
+
output: ["Notes", "3"].join(F) + R + ["Work", "2"].join(F) + R,
|
|
1463
|
+
})
|
|
1464
|
+
// getRecentlyModifiedCounts: c1<F>c7<F>c30
|
|
1465
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1348
1466
|
const stats = manager.getNotesStats();
|
|
1349
1467
|
expect(stats.totalNotes).toBe(5);
|
|
1350
1468
|
expect(stats.accounts).toHaveLength(1);
|
|
@@ -1356,9 +1474,8 @@ describe("AppleNotesManager", () => {
|
|
|
1356
1474
|
it("returns zero counts when no notes exist", () => {
|
|
1357
1475
|
mockExecuteAppleScript
|
|
1358
1476
|
.mockReturnValueOnce({ success: true, output: "iCloud" })
|
|
1359
|
-
.mockReturnValueOnce({ success: true, output: "
|
|
1360
|
-
.mockReturnValueOnce({ success: true, output: "" })
|
|
1361
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1477
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "0"].join(F) + R })
|
|
1478
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1362
1479
|
const stats = manager.getNotesStats();
|
|
1363
1480
|
expect(stats.totalNotes).toBe(0);
|
|
1364
1481
|
expect(stats.recentlyModified.last24h).toBe(0);
|
|
@@ -1368,17 +1485,13 @@ describe("AppleNotesManager", () => {
|
|
|
1368
1485
|
it("handles multiple accounts", () => {
|
|
1369
1486
|
mockExecuteAppleScript
|
|
1370
1487
|
// 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" })
|
|
1488
|
+
.mockReturnValueOnce({ success: true, output: ["iCloud", "Gmail"].join(R) })
|
|
1489
|
+
// iCloud folder counts
|
|
1490
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
|
|
1491
|
+
// Gmail folder counts
|
|
1492
|
+
.mockReturnValueOnce({ success: true, output: ["Notes", "1"].join(F) + R })
|
|
1380
1493
|
// getRecentlyModifiedCounts
|
|
1381
|
-
.mockReturnValueOnce({ success: true, output: "" });
|
|
1494
|
+
.mockReturnValueOnce({ success: true, output: ["0", "0", "0"].join(F) });
|
|
1382
1495
|
const stats = manager.getNotesStats();
|
|
1383
1496
|
expect(stats.totalNotes).toBe(2);
|
|
1384
1497
|
expect(stats.accounts).toHaveLength(2);
|
|
@@ -1393,7 +1506,10 @@ describe("AppleNotesManager", () => {
|
|
|
1393
1506
|
it("returns attachments for a note", () => {
|
|
1394
1507
|
mockExecuteAppleScript.mockReturnValueOnce({
|
|
1395
1508
|
success: true,
|
|
1396
|
-
output:
|
|
1509
|
+
output: [
|
|
1510
|
+
["x-coredata://ABC/ICAttachment/p1", "photo.jpg", "public.jpeg"].join(F),
|
|
1511
|
+
["x-coredata://ABC/ICAttachment/p2", "document.pdf", "com.adobe.pdf"].join(F),
|
|
1512
|
+
].join(R),
|
|
1397
1513
|
});
|
|
1398
1514
|
const attachments = manager.listAttachmentsById("x-coredata://ABC/ICNote/p123");
|
|
1399
1515
|
expect(attachments).toHaveLength(2);
|
|
@@ -1432,7 +1548,7 @@ describe("AppleNotesManager", () => {
|
|
|
1432
1548
|
it("returns attachments for a note by title", () => {
|
|
1433
1549
|
mockExecuteAppleScript.mockReturnValueOnce({
|
|
1434
1550
|
success: true,
|
|
1435
|
-
output: "attach-id
|
|
1551
|
+
output: ["attach-id", "image.png", "public.png"].join(F),
|
|
1436
1552
|
});
|
|
1437
1553
|
const attachments = manager.listAttachments("My Note");
|
|
1438
1554
|
expect(attachments).toHaveLength(1);
|
|
@@ -1463,7 +1579,14 @@ describe("AppleNotesManager", () => {
|
|
|
1463
1579
|
// ---------------------------------------------------------------------------
|
|
1464
1580
|
describe("batchDeleteNotes", () => {
|
|
1465
1581
|
// Helper to create getNoteById mock output (matches AppleScript format)
|
|
1466
|
-
const noteByIdOutput = (title, passwordProtected = false) =>
|
|
1582
|
+
const noteByIdOutput = (title, passwordProtected = false) => [
|
|
1583
|
+
title,
|
|
1584
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1585
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1586
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1587
|
+
"false",
|
|
1588
|
+
String(passwordProtected),
|
|
1589
|
+
].join(F);
|
|
1467
1590
|
it("deletes multiple notes successfully", () => {
|
|
1468
1591
|
// For each note: getNoteById (which isNotePasswordProtectedById also calls), deleteNoteById
|
|
1469
1592
|
mockExecuteAppleScript
|
|
@@ -1548,7 +1671,14 @@ describe("AppleNotesManager", () => {
|
|
|
1548
1671
|
});
|
|
1549
1672
|
describe("batchMoveNotes", () => {
|
|
1550
1673
|
// Helper to create getNoteById mock output (matches AppleScript format)
|
|
1551
|
-
const noteByIdOutput = (title, passwordProtected = false) =>
|
|
1674
|
+
const noteByIdOutput = (title, passwordProtected = false) => [
|
|
1675
|
+
title,
|
|
1676
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1677
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1678
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1679
|
+
"false",
|
|
1680
|
+
String(passwordProtected),
|
|
1681
|
+
].join(F);
|
|
1552
1682
|
it("moves multiple notes successfully", () => {
|
|
1553
1683
|
// For each note: getNoteById, getNoteById (password check), getNoteContentById, create, delete
|
|
1554
1684
|
mockExecuteAppleScript
|
|
@@ -1619,7 +1749,14 @@ describe("AppleNotesManager", () => {
|
|
|
1619
1749
|
// ---------------------------------------------------------------------------
|
|
1620
1750
|
describe("exportNotesAsJson", () => {
|
|
1621
1751
|
// Note details output helper - format: title, id, date, date, shared, passwordProtected
|
|
1622
|
-
const noteDetailsOutput = (title, passwordProtected = false) =>
|
|
1752
|
+
const noteDetailsOutput = (title, passwordProtected = false) => [
|
|
1753
|
+
title,
|
|
1754
|
+
"x-coredata://ABC/ICNote/p1",
|
|
1755
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1756
|
+
"Sunday, January 1, 2025 at 1:00:00 PM",
|
|
1757
|
+
"false",
|
|
1758
|
+
String(passwordProtected),
|
|
1759
|
+
].join(F);
|
|
1623
1760
|
it("exports notes with metadata and content", () => {
|
|
1624
1761
|
mockExecuteAppleScript
|
|
1625
1762
|
// listAccounts
|