@noteplanco/noteplan-mcp 1.1.23 → 1.1.25

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.
Files changed (165) hide show
  1. package/README.md +7 -0
  2. package/dist/index.js +6 -0
  3. package/dist/index.js.map +1 -1
  4. package/dist/noteplan/attachments-paths.d.ts +13 -0
  5. package/dist/noteplan/attachments-paths.d.ts.map +1 -0
  6. package/dist/noteplan/attachments-paths.js +27 -0
  7. package/dist/noteplan/attachments-paths.js.map +1 -0
  8. package/dist/noteplan/embeddings.js +1 -1
  9. package/dist/noteplan/embeddings.js.map +1 -1
  10. package/dist/noteplan/file-reader.d.ts +37 -46
  11. package/dist/noteplan/file-reader.d.ts.map +1 -1
  12. package/dist/noteplan/file-reader.js +200 -202
  13. package/dist/noteplan/file-reader.js.map +1 -1
  14. package/dist/noteplan/file-reader.test.d.ts +2 -0
  15. package/dist/noteplan/file-reader.test.d.ts.map +1 -0
  16. package/dist/noteplan/file-reader.test.js +67 -0
  17. package/dist/noteplan/file-reader.test.js.map +1 -0
  18. package/dist/noteplan/file-writer.d.ts +35 -31
  19. package/dist/noteplan/file-writer.d.ts.map +1 -1
  20. package/dist/noteplan/file-writer.js +280 -164
  21. package/dist/noteplan/file-writer.js.map +1 -1
  22. package/dist/noteplan/file-writer.test.js +704 -191
  23. package/dist/noteplan/file-writer.test.js.map +1 -1
  24. package/dist/noteplan/filter-store.d.ts +5 -5
  25. package/dist/noteplan/filter-store.d.ts.map +1 -1
  26. package/dist/noteplan/filter-store.js +94 -79
  27. package/dist/noteplan/filter-store.js.map +1 -1
  28. package/dist/noteplan/ripgrep-search.d.ts +25 -2
  29. package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
  30. package/dist/noteplan/ripgrep-search.js +75 -2
  31. package/dist/noteplan/ripgrep-search.js.map +1 -1
  32. package/dist/noteplan/space-row-utils.d.ts +20 -0
  33. package/dist/noteplan/space-row-utils.d.ts.map +1 -0
  34. package/dist/noteplan/space-row-utils.js +78 -0
  35. package/dist/noteplan/space-row-utils.js.map +1 -0
  36. package/dist/noteplan/space-row-utils.test.d.ts +2 -0
  37. package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
  38. package/dist/noteplan/space-row-utils.test.js +123 -0
  39. package/dist/noteplan/space-row-utils.test.js.map +1 -0
  40. package/dist/noteplan/sqlite-reader.d.ts +12 -27
  41. package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
  42. package/dist/noteplan/sqlite-reader.js +315 -221
  43. package/dist/noteplan/sqlite-reader.js.map +1 -1
  44. package/dist/noteplan/sqlite-writer.d.ts +1 -1
  45. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  46. package/dist/noteplan/sqlite-writer.js +2 -2
  47. package/dist/noteplan/sqlite-writer.js.map +1 -1
  48. package/dist/noteplan/unified-store.d.ts +41 -30
  49. package/dist/noteplan/unified-store.d.ts.map +1 -1
  50. package/dist/noteplan/unified-store.js +257 -159
  51. package/dist/noteplan/unified-store.js.map +1 -1
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +142 -61
  54. package/dist/server.js.map +1 -1
  55. package/dist/tools/attachments.d.ts +9 -9
  56. package/dist/tools/attachments.d.ts.map +1 -1
  57. package/dist/tools/attachments.js +74 -83
  58. package/dist/tools/attachments.js.map +1 -1
  59. package/dist/tools/attachments.test.js +170 -129
  60. package/dist/tools/attachments.test.js.map +1 -1
  61. package/dist/tools/calendar.d.ts +16 -13
  62. package/dist/tools/calendar.d.ts.map +1 -1
  63. package/dist/tools/calendar.js +17 -16
  64. package/dist/tools/calendar.js.map +1 -1
  65. package/dist/tools/embeddings.d.ts +6 -6
  66. package/dist/tools/embeddings.d.ts.map +1 -1
  67. package/dist/tools/embeddings.js +6 -6
  68. package/dist/tools/embeddings.js.map +1 -1
  69. package/dist/tools/events.d.ts +7 -3
  70. package/dist/tools/events.d.ts.map +1 -1
  71. package/dist/tools/events.js +51 -16
  72. package/dist/tools/events.js.map +1 -1
  73. package/dist/tools/filters.d.ts +28 -33
  74. package/dist/tools/filters.d.ts.map +1 -1
  75. package/dist/tools/filters.js +42 -105
  76. package/dist/tools/filters.js.map +1 -1
  77. package/dist/tools/notes.d.ts +80 -218
  78. package/dist/tools/notes.d.ts.map +1 -1
  79. package/dist/tools/notes.js +180 -177
  80. package/dist/tools/notes.js.map +1 -1
  81. package/dist/tools/notes.test.js +242 -21
  82. package/dist/tools/notes.test.js.map +1 -1
  83. package/dist/tools/search.d.ts +4 -3
  84. package/dist/tools/search.d.ts.map +1 -1
  85. package/dist/tools/search.js +9 -5
  86. package/dist/tools/search.js.map +1 -1
  87. package/dist/tools/search.test.d.ts +2 -0
  88. package/dist/tools/search.test.d.ts.map +1 -0
  89. package/dist/tools/search.test.js +37 -0
  90. package/dist/tools/search.test.js.map +1 -0
  91. package/dist/tools/spaces.d.ts +20 -20
  92. package/dist/tools/spaces.d.ts.map +1 -1
  93. package/dist/tools/spaces.js +28 -28
  94. package/dist/tools/spaces.js.map +1 -1
  95. package/dist/tools/tasks.d.ts +22 -22
  96. package/dist/tools/tasks.d.ts.map +1 -1
  97. package/dist/tools/tasks.js +22 -22
  98. package/dist/tools/tasks.js.map +1 -1
  99. package/dist/tools/templates.d.ts +7 -7
  100. package/dist/tools/templates.d.ts.map +1 -1
  101. package/dist/tools/templates.js +4 -4
  102. package/dist/tools/templates.js.map +1 -1
  103. package/dist/tools/themes.d.ts.map +1 -1
  104. package/dist/tools/themes.js +26 -35
  105. package/dist/tools/themes.js.map +1 -1
  106. package/dist/transport/bridge-availability.d.ts +5 -0
  107. package/dist/transport/bridge-availability.d.ts.map +1 -0
  108. package/dist/transport/bridge-availability.js +92 -0
  109. package/dist/transport/bridge-availability.js.map +1 -0
  110. package/dist/transport/bridge-cascade.d.ts +18 -0
  111. package/dist/transport/bridge-cascade.d.ts.map +1 -0
  112. package/dist/transport/bridge-cascade.js +78 -0
  113. package/dist/transport/bridge-cascade.js.map +1 -0
  114. package/dist/transport/bridge-cascade.test.d.ts +2 -0
  115. package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
  116. package/dist/transport/bridge-cascade.test.js +160 -0
  117. package/dist/transport/bridge-cascade.test.js.map +1 -0
  118. package/dist/transport/bridge-client.d.ts +197 -0
  119. package/dist/transport/bridge-client.d.ts.map +1 -0
  120. package/dist/transport/bridge-client.js +288 -0
  121. package/dist/transport/bridge-client.js.map +1 -0
  122. package/dist/transport/bridge-client.test.d.ts +2 -0
  123. package/dist/transport/bridge-client.test.d.ts.map +1 -0
  124. package/dist/transport/bridge-client.test.js +384 -0
  125. package/dist/transport/bridge-client.test.js.map +1 -0
  126. package/dist/transport/bridge-context.d.ts +10 -0
  127. package/dist/transport/bridge-context.d.ts.map +1 -0
  128. package/dist/transport/bridge-context.js +18 -0
  129. package/dist/transport/bridge-context.js.map +1 -0
  130. package/dist/transport/bridge-fs.d.ts +25 -0
  131. package/dist/transport/bridge-fs.d.ts.map +1 -0
  132. package/dist/transport/bridge-fs.js +129 -0
  133. package/dist/transport/bridge-fs.js.map +1 -0
  134. package/dist/utils/date-utils.d.ts +24 -0
  135. package/dist/utils/date-utils.d.ts.map +1 -1
  136. package/dist/utils/date-utils.js +55 -0
  137. package/dist/utils/date-utils.js.map +1 -1
  138. package/dist/utils/date-utils.test.d.ts +2 -0
  139. package/dist/utils/date-utils.test.d.ts.map +1 -0
  140. package/dist/utils/date-utils.test.js +109 -0
  141. package/dist/utils/date-utils.test.js.map +1 -0
  142. package/dist/utils/folder-access.d.ts +23 -0
  143. package/dist/utils/folder-access.d.ts.map +1 -0
  144. package/dist/utils/folder-access.js +131 -0
  145. package/dist/utils/folder-access.js.map +1 -0
  146. package/dist/utils/folder-access.test.d.ts +2 -0
  147. package/dist/utils/folder-access.test.d.ts.map +1 -0
  148. package/dist/utils/folder-access.test.js +182 -0
  149. package/dist/utils/folder-access.test.js.map +1 -0
  150. package/dist/utils/folder-matcher.d.ts.map +1 -1
  151. package/dist/utils/folder-matcher.js +16 -0
  152. package/dist/utils/folder-matcher.js.map +1 -1
  153. package/dist/utils/folder-matcher.test.js +42 -0
  154. package/dist/utils/folder-matcher.test.js.map +1 -1
  155. package/dist/utils/server-config.d.ts +10 -2
  156. package/dist/utils/server-config.d.ts.map +1 -1
  157. package/dist/utils/server-config.js +16 -2
  158. package/dist/utils/server-config.js.map +1 -1
  159. package/dist/utils/version.d.ts +2 -0
  160. package/dist/utils/version.d.ts.map +1 -1
  161. package/dist/utils/version.js +5 -1
  162. package/dist/utils/version.js.map +1 -1
  163. package/package.json +4 -3
  164. package/scripts/calendar-helper +0 -0
  165. package/scripts/reminders-helper +0 -0
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  // Mock preferences before importing markdown-parser (which reads UserDefaults at runtime)
3
3
  vi.mock('../noteplan/preferences.js', () => ({
4
4
  getTaskMarkerConfigCached: vi.fn(() => ({
@@ -1384,7 +1384,7 @@ describe('matchesFrontmatterProperties – property filter edge cases', () => {
1384
1384
  // ---------------------------------------------------------------------------
1385
1385
  // We test the getNote logic by importing the function and mocking the readers.
1386
1386
  // Since getNote depends on sqliteReader and fileReader, we use vi.mock.
1387
- import { getNote } from '../noteplan/unified-store.js';
1387
+ import { getNote, listNotes } from '../noteplan/unified-store.js';
1388
1388
  import * as sqliteReader from '../noteplan/sqlite-reader.js';
1389
1389
  import * as fileReader from '../noteplan/file-reader.js';
1390
1390
  vi.mock('../noteplan/sqlite-reader.js', () => ({
@@ -1438,36 +1438,36 @@ describe('getNote id/filename resolution', () => {
1438
1438
  beforeEach(() => {
1439
1439
  vi.clearAllMocks();
1440
1440
  });
1441
- it('retrieves a local note via id when SQLite lookup returns null', () => {
1442
- vi.mocked(sqliteReader.getSpaceNote).mockReturnValue(null);
1443
- vi.mocked(fileReader.readNoteFile).mockReturnValue(localNote);
1444
- const result = getNote({ id: 'Notes/Books/my-book.md' });
1441
+ it('retrieves a local note via id when SQLite lookup returns null', async () => {
1442
+ vi.mocked(sqliteReader.getSpaceNote).mockResolvedValue(null);
1443
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(localNote);
1444
+ const result = await getNote({ id: 'Notes/Books/my-book.md' });
1445
1445
  expect(result).toEqual(localNote);
1446
1446
  expect(sqliteReader.getSpaceNote).toHaveBeenCalledWith('Notes/Books/my-book.md');
1447
1447
  expect(fileReader.readNoteFile).toHaveBeenCalledWith('Notes/Books/my-book.md');
1448
1448
  });
1449
- it('retrieves a local note via filename', () => {
1450
- vi.mocked(fileReader.readNoteFile).mockReturnValue(localNote);
1451
- const result = getNote({ filename: 'Notes/Books/my-book.md' });
1449
+ it('retrieves a local note via filename', async () => {
1450
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(localNote);
1451
+ const result = await getNote({ filename: 'Notes/Books/my-book.md' });
1452
1452
  expect(result).toEqual(localNote);
1453
1453
  });
1454
- it('id and filename return the same note for local notes', () => {
1455
- vi.mocked(sqliteReader.getSpaceNote).mockReturnValue(null);
1456
- vi.mocked(fileReader.readNoteFile).mockReturnValue(localNote);
1457
- const byId = getNote({ id: localNote.filename });
1458
- const byFilename = getNote({ filename: localNote.filename });
1454
+ it('id and filename return the same note for local notes', async () => {
1455
+ vi.mocked(sqliteReader.getSpaceNote).mockResolvedValue(null);
1456
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(localNote);
1457
+ const byId = await getNote({ id: localNote.filename });
1458
+ const byFilename = await getNote({ filename: localNote.filename });
1459
1459
  expect(byId).toEqual(byFilename);
1460
1460
  });
1461
- it('returns null for non-existent id', () => {
1462
- vi.mocked(sqliteReader.getSpaceNote).mockReturnValue(null);
1463
- vi.mocked(fileReader.readNoteFile).mockReturnValue(null);
1464
- const result = getNote({ id: 'Notes/nonexistent.md' });
1461
+ it('returns null for non-existent id', async () => {
1462
+ vi.mocked(sqliteReader.getSpaceNote).mockResolvedValue(null);
1463
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(null);
1464
+ const result = await getNote({ id: 'Notes/nonexistent.md' });
1465
1465
  expect(result).toBeNull();
1466
1466
  });
1467
- it('prefers space note when SQLite lookup succeeds', () => {
1467
+ it('prefers space note when SQLite lookup succeeds', async () => {
1468
1468
  const spaceNote = { ...localNote, source: 'space', id: 'space-uuid-123' };
1469
- vi.mocked(sqliteReader.getSpaceNote).mockReturnValue(spaceNote);
1470
- const result = getNote({ id: 'space-uuid-123' });
1469
+ vi.mocked(sqliteReader.getSpaceNote).mockResolvedValue(spaceNote);
1470
+ const result = await getNote({ id: 'space-uuid-123' });
1471
1471
  expect(result).toEqual(spaceNote);
1472
1472
  expect(fileReader.readNoteFile).not.toHaveBeenCalled();
1473
1473
  });
@@ -2477,4 +2477,225 @@ describe('title resolution for search – frontmatter title takes priority', ()
2477
2477
  expect(matchesTitleOrFilename(note, 'knuth reviewer')).toBe(true);
2478
2478
  });
2479
2479
  });
2480
+ // ── listNotes — folder scope inside a space (regression: PR #6 by @steinwaywhw) ──
2481
+ // Ported from PR #6 to the post-bridge async architecture.
2482
+ describe('listNotes – folder scope inside a space', () => {
2483
+ const spaceId = 'space-001';
2484
+ const folderId = 'folder-001';
2485
+ const noteInFolder = {
2486
+ id: 'note-in-folder',
2487
+ title: 'Project Alpha',
2488
+ filename: `%%NotePlanCloud%%/${spaceId}/${folderId}/note-in-folder`,
2489
+ type: 'note',
2490
+ source: 'space',
2491
+ folder: folderId,
2492
+ content: '# Project Alpha',
2493
+ modifiedAt: new Date(),
2494
+ createdAt: new Date(),
2495
+ spaceId,
2496
+ };
2497
+ const noteInOtherFolder = {
2498
+ ...noteInFolder,
2499
+ id: 'note-other',
2500
+ title: 'Other Note',
2501
+ folder: 'folder-002',
2502
+ filename: `%%NotePlanCloud%%/${spaceId}/folder-002/note-other`,
2503
+ };
2504
+ beforeEach(() => {
2505
+ vi.clearAllMocks();
2506
+ vi.mocked(sqliteReader.listSpaces).mockResolvedValue([
2507
+ { id: spaceId, name: 'My Space', noteCount: 2 },
2508
+ ]);
2509
+ vi.mocked(sqliteReader.listSpaceNotes).mockResolvedValue([noteInFolder, noteInOtherFolder]);
2510
+ });
2511
+ it('returns only notes in the resolved folder', async () => {
2512
+ vi.mocked(sqliteReader.resolveSpaceFolder).mockResolvedValue({
2513
+ id: folderId,
2514
+ path: 'Active Projects',
2515
+ name: 'Active Projects',
2516
+ source: 'space',
2517
+ spaceId,
2518
+ });
2519
+ const result = await listNotes({ folder: 'Active Projects', space: 'My Space' });
2520
+ expect(result.map((n) => n.title)).toEqual(['Project Alpha']);
2521
+ expect(sqliteReader.resolveSpaceFolder).toHaveBeenCalledWith(spaceId, 'Active Projects');
2522
+ });
2523
+ it('returns empty when the folder does not resolve in the space', async () => {
2524
+ vi.mocked(sqliteReader.resolveSpaceFolder).mockResolvedValue(null);
2525
+ const result = await listNotes({ folder: 'Nonexistent', space: 'My Space' });
2526
+ expect(result).toHaveLength(0);
2527
+ });
2528
+ it('returns all space notes when no folder is specified', async () => {
2529
+ const result = await listNotes({ space: 'My Space' });
2530
+ expect(result).toHaveLength(2);
2531
+ expect(sqliteReader.resolveSpaceFolder).not.toHaveBeenCalled();
2532
+ });
2533
+ });
2534
+ // ---------------------------------------------------------------------------
2535
+ // Folder access rules — read-side filtering in unified-store
2536
+ // ---------------------------------------------------------------------------
2537
+ import { searchNotes, listFolders, listTags, getCalendarNote, invalidateListingCaches } from '../noteplan/unified-store.js';
2538
+ import { __resetFolderAccessConfigForTests } from '../utils/folder-access.js';
2539
+ describe('folder-access read-side filtering', () => {
2540
+ beforeEach(() => {
2541
+ vi.clearAllMocks();
2542
+ delete process.env.NOTEPLAN_ALLOWED_FOLDERS;
2543
+ delete process.env.NOTEPLAN_DENIED_FOLDERS;
2544
+ __resetFolderAccessConfigForTests();
2545
+ // The listNotes cache is module-level so a previous test's filtered
2546
+ // result would leak into this one. Reset before every case.
2547
+ invalidateListingCaches();
2548
+ // Reset readers to safe defaults — individual tests override.
2549
+ vi.mocked(sqliteReader.listSpaces).mockResolvedValue([]);
2550
+ vi.mocked(sqliteReader.listSpaceNotes).mockResolvedValue([]);
2551
+ vi.mocked(sqliteReader.searchSpaceNotesFTS).mockResolvedValue({ results: [], backend: 'sqlite' });
2552
+ vi.mocked(fileReader.listProjectNotes).mockResolvedValue([]);
2553
+ vi.mocked(fileReader.listCalendarNotes).mockResolvedValue([]);
2554
+ vi.mocked(fileReader.searchLocalNotes).mockResolvedValue([]);
2555
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(null);
2556
+ });
2557
+ afterEach(() => {
2558
+ delete process.env.NOTEPLAN_ALLOWED_FOLDERS;
2559
+ delete process.env.NOTEPLAN_DENIED_FOLDERS;
2560
+ __resetFolderAccessConfigForTests();
2561
+ });
2562
+ it('listNotes drops notes inside a denied folder', async () => {
2563
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Notes/Personal';
2564
+ __resetFolderAccessConfigForTests();
2565
+ const personal = {
2566
+ id: 'Notes/Personal/Diary.md',
2567
+ title: 'Diary',
2568
+ filename: 'Notes/Personal/Diary.md',
2569
+ type: 'note',
2570
+ source: 'local',
2571
+ folder: 'Notes/Personal',
2572
+ content: '',
2573
+ modifiedAt: new Date(),
2574
+ createdAt: new Date(),
2575
+ spaceId: undefined,
2576
+ };
2577
+ const work = { ...personal, id: 'Notes/Work/Plan.md', filename: 'Notes/Work/Plan.md', folder: 'Notes/Work', title: 'Plan' };
2578
+ vi.mocked(fileReader.listProjectNotes).mockResolvedValue([personal, work]);
2579
+ const result = await listNotes();
2580
+ expect(result.map((n) => n.filename)).toEqual(['Notes/Work/Plan.md']);
2581
+ });
2582
+ it('getNote returns null for a denied filename even if the reader has it', async () => {
2583
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Notes/Personal';
2584
+ __resetFolderAccessConfigForTests();
2585
+ const personal = {
2586
+ id: 'Notes/Personal/Diary.md',
2587
+ title: 'Diary',
2588
+ filename: 'Notes/Personal/Diary.md',
2589
+ type: 'note',
2590
+ source: 'local',
2591
+ folder: 'Notes/Personal',
2592
+ content: '',
2593
+ modifiedAt: new Date(),
2594
+ createdAt: new Date(),
2595
+ spaceId: undefined,
2596
+ };
2597
+ vi.mocked(fileReader.readNoteFile).mockResolvedValue(personal);
2598
+ vi.mocked(sqliteReader.getSpaceNote).mockResolvedValue(null);
2599
+ const result = await getNote({ filename: 'Notes/Personal/Diary.md' });
2600
+ expect(result).toBeNull();
2601
+ });
2602
+ it('searchNotes filters denied folders out of the result set', async () => {
2603
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Notes/Personal';
2604
+ __resetFolderAccessConfigForTests();
2605
+ const personal = {
2606
+ id: 'Notes/Personal/Diary.md',
2607
+ title: 'Diary about work',
2608
+ filename: 'Notes/Personal/Diary.md',
2609
+ type: 'note',
2610
+ source: 'local',
2611
+ folder: 'Notes/Personal',
2612
+ content: 'meeting work plan',
2613
+ modifiedAt: new Date(),
2614
+ createdAt: new Date(),
2615
+ spaceId: undefined,
2616
+ };
2617
+ const work = { ...personal, id: 'Notes/Work/Plan.md', filename: 'Notes/Work/Plan.md', folder: 'Notes/Work', title: 'Diary at Work' };
2618
+ // Use the metadata-search branch (searchField=title) so we don't need to
2619
+ // wrangle the ripgrep mock — same filter site applies.
2620
+ vi.mocked(fileReader.listProjectNotes).mockResolvedValue([personal, work]);
2621
+ const result = await searchNotes('diary', { searchField: 'title' });
2622
+ // Both notes have "diary" in their title; the denied one must be filtered out.
2623
+ expect(result.results.map((r) => r.note.filename)).toEqual(['Notes/Work/Plan.md']);
2624
+ });
2625
+ // Folder *listings* themselves used to leak — `noteplan_folders(action: list/find/resolve)`
2626
+ // would return a denied folder by name. The fix prepends `Notes/` to the folder's relative
2627
+ // path so the same env-var match works ("Notes/Personal" rule against `Personal/Diary`).
2628
+ it('listFolders drops local folders inside a denied subtree', async () => {
2629
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Notes/Personal';
2630
+ __resetFolderAccessConfigForTests();
2631
+ vi.mocked(fileReader.listFolders).mockResolvedValue([
2632
+ { path: 'Personal', name: 'Personal', source: 'local' },
2633
+ { path: 'Personal/Finance', name: 'Finance', source: 'local' },
2634
+ { path: 'Work', name: 'Work', source: 'local' },
2635
+ ]);
2636
+ const result = await listFolders();
2637
+ expect(result.map((f) => f.path)).toEqual(['Work']);
2638
+ });
2639
+ // getCalendarNote / getTodayNote / per-day fetches in getNotesInRange used
2640
+ // to call fileReader.getCalendarNote directly, bypassing isFolderAllowed.
2641
+ // Important: a user can deny `Calendar` outright to hide all calendar notes.
2642
+ it('getCalendarNote returns null when Calendar/ is denied', async () => {
2643
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Calendar';
2644
+ __resetFolderAccessConfigForTests();
2645
+ vi.mocked(fileReader.getCalendarNote).mockResolvedValue({
2646
+ id: 'Calendar/20260507.txt',
2647
+ title: '20260507',
2648
+ filename: 'Calendar/20260507.txt',
2649
+ type: 'calendar',
2650
+ source: 'local',
2651
+ folder: 'Calendar',
2652
+ content: 'private',
2653
+ modifiedAt: new Date(),
2654
+ createdAt: new Date(),
2655
+ spaceId: undefined,
2656
+ });
2657
+ const result = await getCalendarNote('20260507');
2658
+ expect(result).toBeNull();
2659
+ });
2660
+ // listTags walked the bridge / ripgrep / filesystem indiscriminately, so
2661
+ // tag NAMES from denied notes leaked through `noteplan_search(action:
2662
+ // list_tags)` even though the notes themselves were filtered. When rules
2663
+ // are configured we now iterate the already-filtered listNotes() output.
2664
+ it('listTags excludes tags that exist only inside a denied folder', async () => {
2665
+ process.env.NOTEPLAN_DENIED_FOLDERS = 'Notes/Personal';
2666
+ __resetFolderAccessConfigForTests();
2667
+ const personal = {
2668
+ id: 'Notes/Personal/Diary.md',
2669
+ title: 'Diary',
2670
+ filename: 'Notes/Personal/Diary.md',
2671
+ type: 'note',
2672
+ source: 'local',
2673
+ folder: 'Notes/Personal',
2674
+ content: 'session with #therapist about #anxiety',
2675
+ modifiedAt: new Date(),
2676
+ createdAt: new Date(),
2677
+ spaceId: undefined,
2678
+ };
2679
+ const work = {
2680
+ ...personal,
2681
+ id: 'Notes/Work/Plan.md',
2682
+ filename: 'Notes/Work/Plan.md',
2683
+ folder: 'Notes/Work',
2684
+ title: 'Plan',
2685
+ content: 'review #q2 plan with #team',
2686
+ };
2687
+ vi.mocked(fileReader.listProjectNotes).mockResolvedValue([personal, work]);
2688
+ const tags = await listTags();
2689
+ expect(tags).toEqual(expect.arrayContaining(['#q2', '#team']));
2690
+ expect(tags).not.toContain('#therapist');
2691
+ expect(tags).not.toContain('#anxiety');
2692
+ });
2693
+ // No rules configured → keep the fast bridge / ripgrep / extractAllTags path.
2694
+ it('listTags uses the fast extractAllTags path when no rules are configured', async () => {
2695
+ vi.mocked(fileReader.extractAllTags).mockResolvedValue(['#fast', '#path']);
2696
+ const tags = await listTags();
2697
+ expect(tags).toEqual(['#fast', '#path']);
2698
+ expect(fileReader.extractAllTags).toHaveBeenCalled();
2699
+ });
2700
+ });
2480
2701
  //# sourceMappingURL=notes.test.js.map