apple-notes-mcp 1.4.3 → 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.
@@ -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();
@@ -242,7 +264,7 @@ describe("buildFolderReference", () => {
242
264
  it("handles special characters in folder names", () => {
243
265
  const result = buildFolderReference("Food & Drink/🥘 Recipes");
244
266
  expect(result).toContain('folder "🥘 Recipes"');
245
- expect(result).toContain('folder "Food & Drink"');
267
+ expect(result).toContain('folder "Food & Drink"');
246
268
  });
247
269
  it("handles escaped slashes in folder names", () => {
248
270
  const result = buildFolderReference("Travel/Spain\\/Portugal 2023");
@@ -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: "Meeting Notes|||x-coredata://ABC/ICNote/p1|||Work|||ITEM|||Project Plan|||x-coredata://ABC/ICNote/p2|||Notes|||ITEM|||Weekly Review|||x-coredata://ABC/ICNote/p3|||Archive",
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("returns empty array on AppleScript error", () => {
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
- const results = manager.searchNotes("test");
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|||x-coredata://ABC/ICNote/p1|||Notes",
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|||x-coredata://ABC/ICNote/p1|||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: "Old Note|||x-coredata://ABC/ICNote/p1|||Recently Deleted|||ITEM|||Active Note|||x-coredata://ABC/ICNote/p2|||Notes",
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|||x-coredata://ABC/ICNote/p1|||Work",
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|||x-coredata://ABC/ICNote/p1|||Notes",
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|||x-coredata://ABC/ICNote/p1|||Notes",
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|||x-coredata://ABC/ICNote/p1|||Notes",
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|||x-coredata://ABC/ICNote/p1|||Notes",
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|||x-coredata://ABC/ICNote/p1|||Work",
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: "Locked Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, true",
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: "Open Note, x-coredata://ABC/ICNote/p2, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, false",
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: "Locked Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, true",
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: "Open Note, x-coredata://ABC/ICNote/p2, date Monday, January 1, 2024 at 12:00:00 PM, date Monday, January 1, 2024 at 12:00:00 PM, false, false",
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: "My Note, x-coredata://ABC123/ICNote/p100, date Saturday, December 27, 2025 at 3:00:00 PM, date Saturday, December 27, 2025 at 4:00:00 PM, false, false",
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: "Shared Note, x-coredata://ABC/ICNote/p1, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, true",
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: "Project Notes, x-coredata://ABC123/ICNote/p200, date Friday, December 20, 2025 at 10:00:00 AM, date Saturday, December 27, 2025 at 2:30:00 PM, false, false",
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: "Note, id123, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, false, false",
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: "Shared Doc, id456, date Monday, January 1, 2025 at 12:00:00 PM, date Monday, January 1, 2025 at 12:00:00 PM, true, false",
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("returns empty array on failure", () => {
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
- const titles = manager.listNotes();
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|||Recent Note 2",
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|||Note 2|||Note 3",
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|||Another 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|||Note 2",
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: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
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: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
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: "My Note, x-coredata://ABC/ICNote/p123, date Monday January 1 2024, date Monday January 1 2024, false, false",
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("returns empty array on failure", () => {
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
- const accounts = manager.listAccounts();
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
- // listFolders for iCloud
1341
- .mockReturnValueOnce({ success: true, output: "id1\tNotes\nid2\tWork" })
1342
- // listNotes for Notes folder
1343
- .mockReturnValueOnce({ success: true, output: "Note 1, Note 2, Note 3" })
1344
- // listNotes for Work folder
1345
- .mockReturnValueOnce({ success: true, output: "Task 1, Task 2" })
1346
- // getRecentlyModifiedCounts
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: "id1\tNotes" })
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
- // listFolders for iCloud
1373
- .mockReturnValueOnce({ success: true, output: "id1\tNotes" })
1374
- // listNotes for iCloud/Notes
1375
- .mockReturnValueOnce({ success: true, output: "Note 1" })
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: "x-coredata://ABC/ICAttachment/p1|||photo.jpg|||public.jpegITEMx-coredata://ABC/ICAttachment/p2|||document.pdf|||com.adobe.pdfITEM",
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|||image.png|||public.pngITEM",
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) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
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) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
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) => `${title}, x-coredata://ABC/ICNote/p1, date Sunday, January 1, 2025 at 1:00:00 PM, date Sunday, January 1, 2025 at 1:00:00 PM, false, ${passwordProtected}`;
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