@noteplanco/noteplan-mcp 1.1.21 → 1.1.24

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 (177) 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/frontmatter-parser.d.ts.map +1 -1
  29. package/dist/noteplan/frontmatter-parser.js +5 -4
  30. package/dist/noteplan/frontmatter-parser.js.map +1 -1
  31. package/dist/noteplan/frontmatter-parser.test.js +44 -0
  32. package/dist/noteplan/frontmatter-parser.test.js.map +1 -1
  33. package/dist/noteplan/markdown-parser.js +1 -1
  34. package/dist/noteplan/markdown-parser.js.map +1 -1
  35. package/dist/noteplan/markdown-parser.test.js +194 -0
  36. package/dist/noteplan/markdown-parser.test.js.map +1 -1
  37. package/dist/noteplan/preferences.d.ts +1 -0
  38. package/dist/noteplan/preferences.d.ts.map +1 -1
  39. package/dist/noteplan/preferences.js +1 -0
  40. package/dist/noteplan/preferences.js.map +1 -1
  41. package/dist/noteplan/ripgrep-search.d.ts +25 -2
  42. package/dist/noteplan/ripgrep-search.d.ts.map +1 -1
  43. package/dist/noteplan/ripgrep-search.js +75 -2
  44. package/dist/noteplan/ripgrep-search.js.map +1 -1
  45. package/dist/noteplan/space-row-utils.d.ts +20 -0
  46. package/dist/noteplan/space-row-utils.d.ts.map +1 -0
  47. package/dist/noteplan/space-row-utils.js +78 -0
  48. package/dist/noteplan/space-row-utils.js.map +1 -0
  49. package/dist/noteplan/space-row-utils.test.d.ts +2 -0
  50. package/dist/noteplan/space-row-utils.test.d.ts.map +1 -0
  51. package/dist/noteplan/space-row-utils.test.js +123 -0
  52. package/dist/noteplan/space-row-utils.test.js.map +1 -0
  53. package/dist/noteplan/sqlite-reader.d.ts +12 -27
  54. package/dist/noteplan/sqlite-reader.d.ts.map +1 -1
  55. package/dist/noteplan/sqlite-reader.js +325 -223
  56. package/dist/noteplan/sqlite-reader.js.map +1 -1
  57. package/dist/noteplan/sqlite-writer.d.ts +1 -1
  58. package/dist/noteplan/sqlite-writer.d.ts.map +1 -1
  59. package/dist/noteplan/sqlite-writer.js +2 -2
  60. package/dist/noteplan/sqlite-writer.js.map +1 -1
  61. package/dist/noteplan/unified-store.d.ts +41 -30
  62. package/dist/noteplan/unified-store.d.ts.map +1 -1
  63. package/dist/noteplan/unified-store.js +307 -161
  64. package/dist/noteplan/unified-store.js.map +1 -1
  65. package/dist/server.d.ts.map +1 -1
  66. package/dist/server.js +143 -62
  67. package/dist/server.js.map +1 -1
  68. package/dist/tools/attachments.d.ts +9 -9
  69. package/dist/tools/attachments.d.ts.map +1 -1
  70. package/dist/tools/attachments.js +74 -83
  71. package/dist/tools/attachments.js.map +1 -1
  72. package/dist/tools/attachments.test.js +170 -129
  73. package/dist/tools/attachments.test.js.map +1 -1
  74. package/dist/tools/calendar.d.ts +36 -13
  75. package/dist/tools/calendar.d.ts.map +1 -1
  76. package/dist/tools/calendar.js +44 -17
  77. package/dist/tools/calendar.js.map +1 -1
  78. package/dist/tools/embeddings.d.ts +6 -6
  79. package/dist/tools/embeddings.d.ts.map +1 -1
  80. package/dist/tools/embeddings.js +6 -6
  81. package/dist/tools/embeddings.js.map +1 -1
  82. package/dist/tools/events.d.ts +7 -3
  83. package/dist/tools/events.d.ts.map +1 -1
  84. package/dist/tools/events.js +51 -16
  85. package/dist/tools/events.js.map +1 -1
  86. package/dist/tools/filters.d.ts +28 -33
  87. package/dist/tools/filters.d.ts.map +1 -1
  88. package/dist/tools/filters.js +42 -105
  89. package/dist/tools/filters.js.map +1 -1
  90. package/dist/tools/notes.d.ts +80 -218
  91. package/dist/tools/notes.d.ts.map +1 -1
  92. package/dist/tools/notes.js +194 -180
  93. package/dist/tools/notes.js.map +1 -1
  94. package/dist/tools/notes.test.js +501 -21
  95. package/dist/tools/notes.test.js.map +1 -1
  96. package/dist/tools/search.d.ts +4 -3
  97. package/dist/tools/search.d.ts.map +1 -1
  98. package/dist/tools/search.js +9 -5
  99. package/dist/tools/search.js.map +1 -1
  100. package/dist/tools/search.test.d.ts +2 -0
  101. package/dist/tools/search.test.d.ts.map +1 -0
  102. package/dist/tools/search.test.js +37 -0
  103. package/dist/tools/search.test.js.map +1 -0
  104. package/dist/tools/spaces.d.ts +20 -20
  105. package/dist/tools/spaces.d.ts.map +1 -1
  106. package/dist/tools/spaces.js +28 -28
  107. package/dist/tools/spaces.js.map +1 -1
  108. package/dist/tools/tasks.d.ts +22 -22
  109. package/dist/tools/tasks.d.ts.map +1 -1
  110. package/dist/tools/tasks.js +22 -22
  111. package/dist/tools/tasks.js.map +1 -1
  112. package/dist/tools/templates.d.ts +7 -7
  113. package/dist/tools/templates.d.ts.map +1 -1
  114. package/dist/tools/templates.js +4 -4
  115. package/dist/tools/templates.js.map +1 -1
  116. package/dist/tools/themes.js +1 -1
  117. package/dist/tools/themes.js.map +1 -1
  118. package/dist/transport/bridge-availability.d.ts +5 -0
  119. package/dist/transport/bridge-availability.d.ts.map +1 -0
  120. package/dist/transport/bridge-availability.js +92 -0
  121. package/dist/transport/bridge-availability.js.map +1 -0
  122. package/dist/transport/bridge-cascade.d.ts +18 -0
  123. package/dist/transport/bridge-cascade.d.ts.map +1 -0
  124. package/dist/transport/bridge-cascade.js +78 -0
  125. package/dist/transport/bridge-cascade.js.map +1 -0
  126. package/dist/transport/bridge-cascade.test.d.ts +2 -0
  127. package/dist/transport/bridge-cascade.test.d.ts.map +1 -0
  128. package/dist/transport/bridge-cascade.test.js +160 -0
  129. package/dist/transport/bridge-cascade.test.js.map +1 -0
  130. package/dist/transport/bridge-client.d.ts +197 -0
  131. package/dist/transport/bridge-client.d.ts.map +1 -0
  132. package/dist/transport/bridge-client.js +288 -0
  133. package/dist/transport/bridge-client.js.map +1 -0
  134. package/dist/transport/bridge-client.test.d.ts +2 -0
  135. package/dist/transport/bridge-client.test.d.ts.map +1 -0
  136. package/dist/transport/bridge-client.test.js +384 -0
  137. package/dist/transport/bridge-client.test.js.map +1 -0
  138. package/dist/transport/bridge-context.d.ts +10 -0
  139. package/dist/transport/bridge-context.d.ts.map +1 -0
  140. package/dist/transport/bridge-context.js +18 -0
  141. package/dist/transport/bridge-context.js.map +1 -0
  142. package/dist/transport/bridge-fs.d.ts +25 -0
  143. package/dist/transport/bridge-fs.d.ts.map +1 -0
  144. package/dist/transport/bridge-fs.js +129 -0
  145. package/dist/transport/bridge-fs.js.map +1 -0
  146. package/dist/utils/date-utils.d.ts +24 -0
  147. package/dist/utils/date-utils.d.ts.map +1 -1
  148. package/dist/utils/date-utils.js +55 -0
  149. package/dist/utils/date-utils.js.map +1 -1
  150. package/dist/utils/date-utils.test.d.ts +2 -0
  151. package/dist/utils/date-utils.test.d.ts.map +1 -0
  152. package/dist/utils/date-utils.test.js +109 -0
  153. package/dist/utils/date-utils.test.js.map +1 -0
  154. package/dist/utils/folder-access.d.ts +23 -0
  155. package/dist/utils/folder-access.d.ts.map +1 -0
  156. package/dist/utils/folder-access.js +131 -0
  157. package/dist/utils/folder-access.js.map +1 -0
  158. package/dist/utils/folder-access.test.d.ts +2 -0
  159. package/dist/utils/folder-access.test.d.ts.map +1 -0
  160. package/dist/utils/folder-access.test.js +182 -0
  161. package/dist/utils/folder-access.test.js.map +1 -0
  162. package/dist/utils/folder-matcher.d.ts.map +1 -1
  163. package/dist/utils/folder-matcher.js +16 -0
  164. package/dist/utils/folder-matcher.js.map +1 -1
  165. package/dist/utils/folder-matcher.test.js +42 -0
  166. package/dist/utils/folder-matcher.test.js.map +1 -1
  167. package/dist/utils/server-config.d.ts +10 -2
  168. package/dist/utils/server-config.d.ts.map +1 -1
  169. package/dist/utils/server-config.js +16 -2
  170. package/dist/utils/server-config.js.map +1 -1
  171. package/dist/utils/version.d.ts +2 -0
  172. package/dist/utils/version.d.ts.map +1 -1
  173. package/dist/utils/version.js +5 -1
  174. package/dist/utils/version.js.map +1 -1
  175. package/package.json +4 -3
  176. package/scripts/calendar-helper +0 -0
  177. package/scripts/reminders-helper +0 -0
@@ -5,18 +5,26 @@ import * as fileWriter from './file-writer.js';
5
5
  import * as sqliteReader from './sqlite-reader.js';
6
6
  import * as sqliteWriter from './sqlite-writer.js';
7
7
  import * as frontmatter from './frontmatter-parser.js';
8
+ import { extractTagsFromContent } from './markdown-parser.js';
8
9
  import { getTodayDateString, parseFlexibleDate } from '../utils/date-utils.js';
9
10
  import { matchFolder } from '../utils/folder-matcher.js';
10
11
  import { searchWithRipgrep, isRipgrepAvailable } from './ripgrep-search.js';
11
12
  import { fuzzySearch } from './fuzzy-search.js';
12
13
  import { parseFlexibleDateFilter, isDateInRange } from '../utils/date-filters.js';
13
14
  import { normalizeFilename } from '../utils/filename-normalize.js';
15
+ import { isFolderAllowed, hasFolderAccessRules } from '../utils/folder-access.js';
16
+ import { getBridgeClient } from '../transport/bridge-availability.js';
14
17
  // Cache ripgrep availability check
15
18
  let ripgrepAvailable = null;
16
19
  const LIST_NOTES_CACHE_TTL_MS = 5000;
17
20
  const LIST_FOLDERS_CACHE_TTL_MS = 15000;
21
+ // Tag listing crosses the bridge for local tags (fast via NoteCache) and
22
+ // then iterates every TeamSpace note's content via regex (slow). Cache
23
+ // the merged result per scope so repeat calls within the TTL are instant.
24
+ const LIST_TAGS_CACHE_TTL_MS = 60_000;
18
25
  const listNotesCache = new Map();
19
26
  const listFoldersCache = new Map();
27
+ const listTagsCache = new Map();
20
28
  function getCachedValue(cache, key) {
21
29
  const entry = cache.get(key);
22
30
  if (!entry)
@@ -34,9 +42,10 @@ function setCachedValue(cache, key, value, ttlMs) {
34
42
  });
35
43
  return value;
36
44
  }
37
- function invalidateListingCaches() {
45
+ export function invalidateListingCaches() {
38
46
  listNotesCache.clear();
39
47
  listFoldersCache.clear();
48
+ listTagsCache.clear();
40
49
  }
41
50
  function normalizeLocalFolderFilter(folder) {
42
51
  if (!folder)
@@ -60,13 +69,13 @@ function normalizeLocalFolderFilter(folder) {
60
69
  * Returns undefined when the input is undefined/empty (pass-through for optional params).
61
70
  * Throws when a non-empty value doesn't match any known space.
62
71
  */
63
- export function resolveSpaceId(space) {
72
+ export async function resolveSpaceId(space) {
64
73
  if (!space)
65
74
  return undefined;
66
75
  const trimmed = space.trim();
67
76
  if (!trimmed)
68
77
  return undefined;
69
- const spaces = sqliteReader.listSpaces();
78
+ const spaces = await sqliteReader.listSpaces();
70
79
  // Exact ID match takes priority (unambiguous)
71
80
  const idMatch = spaces.find((s) => s.id === trimmed);
72
81
  if (idMatch)
@@ -84,19 +93,30 @@ export function resolveSpaceId(space) {
84
93
  throw new Error(`Space not found: "${space}". Available spaces: ${available.length > 0 ? available.join(', ') : 'none'}`);
85
94
  }
86
95
  /**
87
- * Get a note by various identifiers
96
+ * Get a note by various identifiers. Outer wrapper applies the
97
+ * configured folder allow/deny rules — pretending the note doesn't
98
+ * exist if it sits inside a blocked folder, so the agent can't sneak
99
+ * around the filter via id/filename/date/title resolution.
88
100
  */
89
- export function getNote(options) {
101
+ export async function getNote(options) {
102
+ const note = await getNoteUnchecked(options);
103
+ if (!note)
104
+ return null;
105
+ if (!isFolderAllowed(note.filename))
106
+ return null;
107
+ return note;
108
+ }
109
+ async function getNoteUnchecked(options) {
90
110
  const { id, title, filename, date } = options;
91
- const space = resolveSpaceId(options.space);
111
+ const space = await resolveSpaceId(options.space);
92
112
  // If ID is specified, get directly (best for space notes)
93
113
  if (id) {
94
114
  const normalizedId = normalizeFilename(id);
95
- const spaceNote = sqliteReader.getSpaceNote(normalizedId);
115
+ const spaceNote = await sqliteReader.getSpaceNote(normalizedId);
96
116
  if (spaceNote)
97
117
  return spaceNote;
98
118
  // Fallback: for local notes, id === filename
99
- const localNote = fileReader.readNoteFile(normalizedId);
119
+ const localNote = await fileReader.readNoteFile(normalizedId);
100
120
  if (localNote)
101
121
  return localNote;
102
122
  return null;
@@ -105,7 +125,7 @@ export function getNote(options) {
105
125
  if (date) {
106
126
  const dateStr = parseFlexibleDate(date);
107
127
  if (space) {
108
- return sqliteReader.getSpaceCalendarNote(dateStr, space);
128
+ return await sqliteReader.getSpaceCalendarNote(dateStr, space);
109
129
  }
110
130
  return fileReader.getCalendarNote(dateStr);
111
131
  }
@@ -113,46 +133,44 @@ export function getNote(options) {
113
133
  if (filename) {
114
134
  const normalizedFilename = normalizeFilename(filename.trim());
115
135
  if (space) {
116
- const directSpaceNote = sqliteReader.getSpaceNote(normalizedFilename);
136
+ const directSpaceNote = await sqliteReader.getSpaceNote(normalizedFilename);
117
137
  if (directSpaceNote && directSpaceNote.spaceId === space) {
118
138
  return directSpaceNote;
119
139
  }
120
- const scopedSpaceNote = sqliteReader
121
- .listSpaceNotes(space)
122
- .find((note) => note.filename === normalizedFilename || note.id === normalizedFilename);
140
+ const scopedSpaceNote = (await sqliteReader.listSpaceNotes(space)).find((note) => note.filename === normalizedFilename || note.id === normalizedFilename);
123
141
  if (scopedSpaceNote) {
124
142
  return scopedSpaceNote;
125
143
  }
126
144
  }
127
145
  // Check if it's a space filename
128
146
  if (normalizedFilename.includes('%%NotePlanCloud%%')) {
129
- return sqliteReader.getSpaceNote(normalizedFilename);
147
+ return await sqliteReader.getSpaceNote(normalizedFilename);
130
148
  }
131
- const localNote = fileReader.readNoteFile(normalizedFilename);
149
+ const localNote = await fileReader.readNoteFile(normalizedFilename);
132
150
  if (localNote)
133
151
  return localNote;
134
- return sqliteReader.getSpaceNote(normalizedFilename);
152
+ return await sqliteReader.getSpaceNote(normalizedFilename);
135
153
  }
136
154
  // If title is specified, search by title
137
155
  if (title) {
138
156
  if (space) {
139
- return sqliteReader.getSpaceNoteByTitle(title, space);
157
+ return await sqliteReader.getSpaceNoteByTitle(title, space);
140
158
  }
141
159
  // Try local first
142
- const localNote = fileReader.getNoteByTitle(title);
160
+ const localNote = await fileReader.getNoteByTitle(title);
143
161
  if (localNote)
144
162
  return localNote;
145
163
  // Try space
146
- return sqliteReader.getSpaceNoteByTitle(title);
164
+ return await sqliteReader.getSpaceNoteByTitle(title);
147
165
  }
148
166
  return null;
149
167
  }
150
168
  /**
151
169
  * List all notes, optionally filtered
152
170
  */
153
- export function listNotes(options = {}) {
171
+ export async function listNotes(options = {}) {
154
172
  const { folder, type } = options;
155
- const space = resolveSpaceId(options.space);
173
+ const space = await resolveSpaceId(options.space);
156
174
  const normalizedFolder = normalizeLocalFolderFilter(folder);
157
175
  const cacheKey = JSON.stringify({
158
176
  folder: normalizedFolder || '',
@@ -168,30 +186,37 @@ export function listNotes(options = {}) {
168
186
  // Get local notes
169
187
  if (!space) {
170
188
  if (!type || type === 'note') {
171
- notes.push(...fileReader.listProjectNotes(normalizedFolder));
189
+ notes.push(...(await fileReader.listProjectNotes(normalizedFolder)));
172
190
  }
173
191
  if ((!type || type === 'calendar') && !hasFolderScope) {
174
- notes.push(...fileReader.listCalendarNotes());
192
+ notes.push(...(await fileReader.listCalendarNotes()));
175
193
  }
176
194
  }
177
195
  // Get space notes
178
196
  if (space || !hasFolderScope) {
179
- notes.push(...sqliteReader.listSpaceNotes(space));
197
+ let spaceNotes = await sqliteReader.listSpaceNotes(space);
198
+ if (space && hasFolderScope) {
199
+ const resolved = await sqliteReader.resolveSpaceFolder(space, normalizedFolder);
200
+ spaceNotes = resolved ? spaceNotes.filter((n) => n.folder === resolved.id) : [];
201
+ }
202
+ notes.push(...spaceNotes);
180
203
  }
181
- // Sort by modified date (newest first)
182
- notes.sort((a, b) => {
204
+ const accessFiltered = notes.filter((n) => isFolderAllowed(n.filename));
205
+ // Sort by modified date (newest first) AFTER folder filtering — no
206
+ // point sorting entries we're about to discard.
207
+ accessFiltered.sort((a, b) => {
183
208
  const dateA = a.modifiedAt?.getTime() || 0;
184
209
  const dateB = b.modifiedAt?.getTime() || 0;
185
210
  return dateB - dateA;
186
211
  });
187
- return setCachedValue(listNotesCache, cacheKey, notes, LIST_NOTES_CACHE_TTL_MS);
212
+ return setCachedValue(listNotesCache, cacheKey, accessFiltered, LIST_NOTES_CACHE_TTL_MS);
188
213
  }
189
214
  /**
190
215
  * Search across all notes with enhanced options
191
216
  */
192
217
  export async function searchNotes(query, options = {}) {
193
218
  const { types, folder, limit = 50, fuzzy = false } = options;
194
- const space = resolveSpaceId(options.space);
219
+ const space = await resolveSpaceId(options.space);
195
220
  const normalizedFolder = normalizeLocalFolderFilter(folder);
196
221
  const searchField = options.searchField ?? 'content';
197
222
  const effectiveTypes = searchField === 'content' ? types : (types ?? ['note']);
@@ -238,18 +263,18 @@ export async function searchNotes(query, options = {}) {
238
263
  paths: searchPaths,
239
264
  maxResults: limit * 2, // Get extra for filtering
240
265
  });
241
- backend = 'ripgrep';
266
+ backend = rgResult.backend;
242
267
  partialResults = rgResult.partialResults;
243
268
  if (rgResult.warning)
244
269
  warnings.push(rgResult.warning);
245
- results.push(...convertRipgrepToSearchResults(rgResult.matches));
270
+ results.push(...(await convertRipgrepToSearchResults(rgResult.matches)));
246
271
  }
247
272
  catch (error) {
248
273
  console.error('Ripgrep search failed:', error);
249
274
  backend = 'fallback';
250
275
  warnings.push('ripgrep failed; using fallback local search');
251
276
  // Fall back to simple search
252
- const localNotes = fileReader.searchLocalNotes(query, {
277
+ const localNotes = await fileReader.searchLocalNotes(query, {
253
278
  types: effectiveTypes,
254
279
  folder: normalizedFolder,
255
280
  limit: limit * 2,
@@ -261,7 +286,7 @@ export async function searchNotes(query, options = {}) {
261
286
  backend = 'simple';
262
287
  warnings.push('ripgrep unavailable; using fallback local search');
263
288
  // Fallback to original search method
264
- const localNotes = fileReader.searchLocalNotes(query, {
289
+ const localNotes = await fileReader.searchLocalNotes(query, {
265
290
  types: effectiveTypes,
266
291
  folder: normalizedFolder,
267
292
  limit: limit * 2,
@@ -270,7 +295,7 @@ export async function searchNotes(query, options = {}) {
270
295
  }
271
296
  }
272
297
  // Search space notes
273
- const spaceNotes = sqliteReader.searchSpaceNotesFTS(query, { spaceId: space, limit: limit * 2 });
298
+ const spaceNotes = await sqliteReader.searchSpaceNotesFTS(query, { spaceId: space, limit: limit * 2 });
274
299
  for (const note of spaceNotes) {
275
300
  results.push({
276
301
  note,
@@ -278,18 +303,27 @@ export async function searchNotes(query, options = {}) {
278
303
  score: 50, // Base score, will be enhanced below
279
304
  });
280
305
  }
306
+ // Space-only search: relabel backend so callers can tell whether the
307
+ // space query went through bridge or sqlite-direct. Local content
308
+ // search (ripgrep/fallback) didn't run in this branch.
309
+ if (space) {
310
+ backend = (await getBridgeClient()) ? 'bridge' : 'sqlite';
311
+ }
281
312
  }
282
313
  else {
283
314
  backend = 'simple';
284
315
  warnings.push(`searchField=${searchField} performs metadata matching on titles/filenames (not full-text content search).`);
285
- const candidates = listNotes({
316
+ const candidates = (await listNotes({
286
317
  folder: normalizedFolder,
287
318
  space,
288
- }).filter((note) => !effectiveTypes || effectiveTypes.includes(note.type));
319
+ })).filter((note) => !effectiveTypes || effectiveTypes.includes(note.type));
289
320
  results = candidates
290
321
  .map((note) => scoreMetadataMatch(note, query, searchField, options.caseSensitive === true))
291
322
  .filter((entry) => entry !== null);
292
323
  }
324
+ // Drop denied folders before any per-result work (frontmatter parsing,
325
+ // date math, scoring) — they're going out the window anyway.
326
+ results = results.filter((r) => isFolderAllowed(r.note.filename));
293
327
  // Apply date filters
294
328
  if (modifiedAfter || modifiedBefore || createdAfter || createdBefore) {
295
329
  results = results.filter((r) => {
@@ -333,6 +367,9 @@ export async function searchNotes(query, options = {}) {
333
367
  warnings,
334
368
  };
335
369
  }
370
+ /**
371
+ * Split query on `|` for OR alternatives.
372
+ */
336
373
  function splitSearchTerms(query) {
337
374
  const tokens = query
338
375
  .split('|')
@@ -340,6 +377,13 @@ function splitSearchTerms(query) {
340
377
  .filter(Boolean);
341
378
  return tokens.length > 0 ? tokens : [query.trim()];
342
379
  }
380
+ /**
381
+ * Normalize a string for metadata matching: replace underscores with spaces
382
+ * so "knuth_reviewer" and "knuth reviewer" are treated equivalently.
383
+ */
384
+ function normalizeForMetadataMatch(value) {
385
+ return value.replace(/_/g, ' ');
386
+ }
343
387
  function metadataScore(value, term) {
344
388
  if (!value || !term)
345
389
  return 0;
@@ -354,6 +398,44 @@ function metadataScore(value, term) {
354
398
  return 80;
355
399
  return 0;
356
400
  }
401
+ /**
402
+ * Score a metadata term against a value with token-aware AND matching.
403
+ *
404
+ * Matching rules (mirrors NotePlan's note search behavior):
405
+ * - Single word → substring match (with underscores normalized to spaces)
406
+ * - Multiple space-separated words → AND: all words must appear in the value
407
+ * (in any order, underscores treated as spaces)
408
+ * - Use `|` between terms for OR matching (handled by splitSearchTerms)
409
+ *
410
+ * Returns 0 if no match, or a positive score based on match quality.
411
+ */
412
+ function metadataScoreTokenAware(value, term) {
413
+ if (!value || !term)
414
+ return 0;
415
+ // Normalize underscores to spaces in both value and term
416
+ const normalizedValue = normalizeForMetadataMatch(value);
417
+ const normalizedTerm = normalizeForMetadataMatch(term);
418
+ // Split on spaces into AND tokens
419
+ const words = normalizedTerm.split(/\s+/).filter(Boolean);
420
+ if (words.length <= 1) {
421
+ // Single word: try both original and normalized value
422
+ const directScore = metadataScore(value, term);
423
+ const normalizedScore = metadataScore(normalizedValue, normalizedTerm);
424
+ return Math.max(directScore, normalizedScore);
425
+ }
426
+ // Multiple words: ALL must appear in the normalized value (AND)
427
+ const allMatch = words.every((word) => normalizedValue.includes(word));
428
+ if (!allMatch)
429
+ return 0;
430
+ // Score based on how well the terms match
431
+ // Exact match of full query (after normalization)
432
+ if (normalizedValue === normalizedTerm)
433
+ return 120;
434
+ if (normalizedValue.startsWith(normalizedTerm))
435
+ return 100;
436
+ // All tokens present → good contains match
437
+ return 70 + Math.min(words.length, 10);
438
+ }
357
439
  function scoreMetadataMatch(note, rawQuery, searchField, caseSensitive) {
358
440
  const terms = splitSearchTerms(rawQuery);
359
441
  const title = caseSensitive ? note.title : note.title.toLowerCase();
@@ -365,14 +447,14 @@ function scoreMetadataMatch(note, rawQuery, searchField, caseSensitive) {
365
447
  if (!term)
366
448
  continue;
367
449
  if (searchField === 'title' || searchField === 'title_or_filename') {
368
- const score = metadataScore(title, term);
450
+ const score = metadataScoreTokenAware(title, term);
369
451
  if (score > bestScore) {
370
452
  bestScore = score;
371
453
  matchedOn = 'title';
372
454
  }
373
455
  }
374
456
  if (searchField === 'filename' || searchField === 'title_or_filename') {
375
- const score = metadataScore(filename, term);
457
+ const score = metadataScoreTokenAware(filename, term);
376
458
  if (score > bestScore) {
377
459
  bestScore = score;
378
460
  matchedOn = 'filename';
@@ -445,7 +527,7 @@ function noteToSearchResult(note, query) {
445
527
  /**
446
528
  * Convert ripgrep matches to SearchResults
447
529
  */
448
- function convertRipgrepToSearchResults(matches) {
530
+ async function convertRipgrepToSearchResults(matches) {
449
531
  // Group matches by file
450
532
  const byFile = new Map();
451
533
  for (const m of matches) {
@@ -455,7 +537,7 @@ function convertRipgrepToSearchResults(matches) {
455
537
  }
456
538
  const results = [];
457
539
  for (const [file, fileMatches] of byFile) {
458
- const note = fileReader.readNoteFile(file);
540
+ const note = await fileReader.readNoteFile(file);
459
541
  if (note) {
460
542
  results.push({
461
543
  note,
@@ -574,8 +656,38 @@ function calculateScore(note, matches, query) {
574
656
  /**
575
657
  * Create a new note with smart folder matching
576
658
  */
577
- export function createNote(title, content, options = {}) {
578
- const { folder, space, createNewFolder = false } = options;
659
+ export async function createNote(title, content, options = {}) {
660
+ const { folder, space, createNewFolder = false, filename, calendarDate } = options;
661
+ // Calendar-note path: short-circuit BEFORE the project-note logic so
662
+ // we don't pass a missing title through sanitizeFilename, which used
663
+ // to surface as `Cannot read properties of undefined (reading 'replace')`.
664
+ if (calendarDate) {
665
+ if (space) {
666
+ throw new Error('Calendar notes cannot live inside a TeamSpace via this tool — omit `space` and target the local Calendar folder.');
667
+ }
668
+ if (filename) {
669
+ throw new Error('Calendar notes derive their filename from the date — pass `date` instead of `filename`.');
670
+ }
671
+ const writtenFilename = await fileWriter.createCalendarNoteIfNew(calendarDate, content ?? '');
672
+ const note = await fileReader.readNoteFile(writtenFilename);
673
+ if (!note)
674
+ throw new Error('Failed to create calendar note');
675
+ invalidateListingCaches();
676
+ const calendarFolderResolution = {
677
+ requested: 'Calendar',
678
+ resolved: 'Calendar',
679
+ matched: true,
680
+ ambiguous: false,
681
+ score: 1,
682
+ alternatives: [],
683
+ };
684
+ return { note, folderResolution: calendarFolderResolution };
685
+ }
686
+ // Project- and space-note path requires a non-empty title; reject up
687
+ // front with a clear error rather than crashing in sanitizeFilename.
688
+ if (!title?.trim()) {
689
+ throw new Error('title is required for project notes. To create a calendar note instead, pass `date` (e.g. "2026-W16", "2026-05-07", "today").');
690
+ }
579
691
  // Initialize folder resolution info
580
692
  const folderResolution = {
581
693
  requested: folder,
@@ -606,10 +718,16 @@ export function createNote(title, content, options = {}) {
606
718
  }
607
719
  }
608
720
  }
609
- const resolvedSpace = resolveSpaceId(space);
721
+ const resolvedSpace = await resolveSpaceId(space);
610
722
  if (resolvedSpace) {
611
- const filename = sqliteWriter.createSpaceNote(resolvedSpace, title, effectiveContent);
612
- const note = sqliteReader.getSpaceNote(filename);
723
+ // Space notes don't use filesystem filenames; the explicit-filename
724
+ // override is a local-note feature only. Surface that explicitly
725
+ // rather than silently dropping the parameter.
726
+ if (filename) {
727
+ throw new Error('filename is not supported for space notes — they are addressed by ID, not by filesystem path.');
728
+ }
729
+ const writtenFilename = sqliteWriter.createSpaceNote(resolvedSpace, title, effectiveContent);
730
+ const note = await sqliteReader.getSpaceNote(writtenFilename);
613
731
  if (!note)
614
732
  throw new Error('Failed to create space note');
615
733
  invalidateListingCaches();
@@ -618,7 +736,7 @@ export function createNote(title, content, options = {}) {
618
736
  // Smart folder matching for local notes
619
737
  let resolvedFolder = folder;
620
738
  if (folder && !createNewFolder) {
621
- const folders = fileReader.listFolders();
739
+ const folders = await fileReader.listFolders();
622
740
  const match = matchFolder(folder, folders);
623
741
  if (match.matched && match.folder) {
624
742
  resolvedFolder = match.folder.path;
@@ -629,8 +747,8 @@ export function createNote(title, content, options = {}) {
629
747
  folderResolution.alternatives = match.alternatives.map((f) => f.path);
630
748
  }
631
749
  }
632
- const filename = fileWriter.createProjectNote(title, effectiveContent, resolvedFolder);
633
- const note = fileReader.readNoteFile(filename);
750
+ const writtenFilename = await fileWriter.createProjectNote(title, effectiveContent, resolvedFolder, filename);
751
+ const note = await fileReader.readNoteFile(writtenFilename);
634
752
  if (!note)
635
753
  throw new Error('Failed to create note');
636
754
  invalidateListingCaches();
@@ -639,23 +757,23 @@ export function createNote(title, content, options = {}) {
639
757
  /**
640
758
  * Update a note's content
641
759
  */
642
- export function updateNote(identifier, content, options = {}) {
643
- const updateSpace = (spaceIdentifier) => {
644
- const existing = sqliteReader.getSpaceNote(spaceIdentifier);
760
+ export async function updateNote(identifier, content, options = {}) {
761
+ const updateSpace = async (spaceIdentifier) => {
762
+ const existing = await sqliteReader.getSpaceNote(spaceIdentifier);
645
763
  if (!existing) {
646
764
  throw new Error(`Note not found: ${spaceIdentifier}`);
647
765
  }
648
766
  const writeIdentifier = existing.id || spaceIdentifier;
649
767
  sqliteWriter.updateSpaceNote(writeIdentifier, content);
650
- const note = sqliteReader.getSpaceNote(writeIdentifier);
768
+ const note = await sqliteReader.getSpaceNote(writeIdentifier);
651
769
  if (!note)
652
770
  throw new Error('Note not found after update');
653
771
  invalidateListingCaches();
654
772
  return note;
655
773
  };
656
- const updateLocal = (localIdentifier) => {
657
- fileWriter.updateNote(localIdentifier, content);
658
- const note = fileReader.readNoteFile(localIdentifier);
774
+ const updateLocal = async (localIdentifier) => {
775
+ await fileWriter.updateNote(localIdentifier, content);
776
+ const note = await fileReader.readNoteFile(localIdentifier);
659
777
  if (!note)
660
778
  throw new Error('Note not found after update');
661
779
  invalidateListingCaches();
@@ -670,8 +788,8 @@ export function updateNote(identifier, content, options = {}) {
670
788
  if (identifier.includes('%%NotePlanCloud%%')) {
671
789
  return updateSpace(identifier);
672
790
  }
673
- const localNote = fileReader.readNoteFile(identifier);
674
- const spaceNote = sqliteReader.getSpaceNote(identifier);
791
+ const localNote = await fileReader.readNoteFile(identifier);
792
+ const spaceNote = await sqliteReader.getSpaceNote(identifier);
675
793
  if (localNote) {
676
794
  return updateLocal(localNote.filename);
677
795
  }
@@ -683,8 +801,8 @@ export function updateNote(identifier, content, options = {}) {
683
801
  /**
684
802
  * Delete a note
685
803
  */
686
- export function deleteNote(identifier) {
687
- const note = getNoteByIdentifierOrThrow(identifier);
804
+ export async function deleteNote(identifier) {
805
+ const note = await getNoteByIdentifierOrThrow(identifier);
688
806
  if (note.source === 'space') {
689
807
  const moved = sqliteWriter.deleteSpaceNote(note.id || note.filename);
690
808
  invalidateListingCaches();
@@ -695,7 +813,7 @@ export function deleteNote(identifier) {
695
813
  noteId: moved.noteId,
696
814
  };
697
815
  }
698
- const trashedPath = fileWriter.deleteNote(note.filename);
816
+ const trashedPath = await fileWriter.deleteNote(note.filename);
699
817
  invalidateListingCaches();
700
818
  return {
701
819
  source: 'local',
@@ -703,8 +821,8 @@ export function deleteNote(identifier) {
703
821
  toIdentifier: trashedPath,
704
822
  };
705
823
  }
706
- function getNoteByIdentifierOrThrow(identifier, options = {}) {
707
- const note = getNote({ id: identifier }) ?? getNote({ filename: identifier });
824
+ async function getNoteByIdentifierOrThrow(identifier, options = {}) {
825
+ const note = (await getNote({ id: identifier })) ?? (await getNote({ filename: identifier }));
708
826
  if (!note) {
709
827
  throw new Error('Note not found');
710
828
  }
@@ -713,8 +831,8 @@ function getNoteByIdentifierOrThrow(identifier, options = {}) {
713
831
  }
714
832
  return note;
715
833
  }
716
- function getLocalProjectNoteOrThrow(identifier, action) {
717
- const note = getNoteByIdentifierOrThrow(identifier);
834
+ async function getLocalProjectNoteOrThrow(identifier, action) {
835
+ const note = await getNoteByIdentifierOrThrow(identifier);
718
836
  if (note.source !== 'local') {
719
837
  throw new Error(`${action} is currently supported for local notes only`);
720
838
  }
@@ -723,7 +841,7 @@ function getLocalProjectNoteOrThrow(identifier, action) {
723
841
  }
724
842
  return note;
725
843
  }
726
- function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
844
+ async function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
727
845
  const query = destinationFolder.trim();
728
846
  if (!query) {
729
847
  throw new Error('Destination folder is required');
@@ -738,7 +856,7 @@ function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
738
856
  label: note.spaceId,
739
857
  };
740
858
  }
741
- const folder = sqliteReader.resolveSpaceFolder(note.spaceId, query, { includeTrash: true });
859
+ const folder = await sqliteReader.resolveSpaceFolder(note.spaceId, query, { includeTrash: true });
742
860
  if (!folder?.id) {
743
861
  throw new Error(`Destination folder not found in space: ${destinationFolder}`);
744
862
  }
@@ -750,13 +868,13 @@ function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
750
868
  label: folder.path,
751
869
  };
752
870
  }
753
- export function previewMoveNote(identifier, destinationFolder) {
754
- const note = getNoteByIdentifierOrThrow(identifier);
871
+ export async function previewMoveNote(identifier, destinationFolder) {
872
+ const note = await getNoteByIdentifierOrThrow(identifier);
755
873
  if (note.source === 'space') {
756
874
  if (note.type !== 'note') {
757
875
  throw new Error('Moving calendar notes in TeamSpaces is not supported');
758
876
  }
759
- const destination = resolveSpaceMoveDestination(note, destinationFolder);
877
+ const destination = await resolveSpaceMoveDestination(note, destinationFolder);
760
878
  return {
761
879
  note,
762
880
  fromFilename: note.filename,
@@ -765,20 +883,20 @@ export function previewMoveNote(identifier, destinationFolder) {
765
883
  destinationParentId: destination.id,
766
884
  };
767
885
  }
768
- const preview = fileWriter.previewMoveLocalNote(note.filename, destinationFolder);
886
+ const preview = await fileWriter.previewMoveLocalNote(note.filename, destinationFolder);
769
887
  return {
770
888
  note,
771
889
  ...preview,
772
890
  };
773
891
  }
774
- export function moveNote(identifier, destinationFolder) {
775
- const preview = previewMoveNote(identifier, destinationFolder);
892
+ export async function moveNote(identifier, destinationFolder) {
893
+ const preview = await previewMoveNote(identifier, destinationFolder);
776
894
  if (preview.note.source === 'space') {
777
895
  if (!preview.note.id || !preview.destinationParentId) {
778
896
  throw new Error('Could not resolve TeamSpace move target');
779
897
  }
780
898
  sqliteWriter.moveSpaceNote(preview.note.id, preview.destinationParentId);
781
- const movedNote = sqliteReader.getSpaceNote(preview.note.id);
899
+ const movedNote = await sqliteReader.getSpaceNote(preview.note.id);
782
900
  if (!movedNote) {
783
901
  throw new Error('Failed to read note after move');
784
902
  }
@@ -791,8 +909,8 @@ export function moveNote(identifier, destinationFolder) {
791
909
  destinationParentId: preview.destinationParentId,
792
910
  };
793
911
  }
794
- const nextFilename = fileWriter.moveLocalNote(preview.note.filename, destinationFolder);
795
- const movedNote = fileReader.readNoteFile(nextFilename);
912
+ const nextFilename = await fileWriter.moveLocalNote(preview.note.filename, destinationFolder);
913
+ const movedNote = await fileReader.readNoteFile(nextFilename);
796
914
  if (!movedNote) {
797
915
  throw new Error('Failed to read note after move');
798
916
  }
@@ -804,17 +922,17 @@ export function moveNote(identifier, destinationFolder) {
804
922
  destinationFolder: preview.destinationFolder,
805
923
  };
806
924
  }
807
- export function previewRestoreNote(identifier, destinationFolder) {
808
- const note = getNoteByIdentifierOrThrow(identifier, { allowTrash: true });
925
+ export async function previewRestoreNote(identifier, destinationFolder) {
926
+ const note = await getNoteByIdentifierOrThrow(identifier, { allowTrash: true });
809
927
  if (note.source === 'space') {
810
928
  if (!note.id) {
811
929
  throw new Error('Space note ID is required for restore');
812
930
  }
813
- if (!sqliteReader.isSpaceNoteInTrash(note.id)) {
931
+ if (!await sqliteReader.isSpaceNoteInTrash(note.id)) {
814
932
  throw new Error('Note is not in TeamSpace @Trash');
815
933
  }
816
934
  const destination = destinationFolder
817
- ? resolveSpaceMoveDestination(note, destinationFolder)
935
+ ? await resolveSpaceMoveDestination(note, destinationFolder)
818
936
  : { id: note.spaceId || '', label: note.spaceId || '' };
819
937
  if (!destination.id) {
820
938
  throw new Error('Could not resolve TeamSpace restore destination');
@@ -829,8 +947,8 @@ export function previewRestoreNote(identifier, destinationFolder) {
829
947
  if (note.type !== 'trash') {
830
948
  throw new Error('Local note is not in @Trash');
831
949
  }
832
- const preview = fileWriter.previewRestoreLocalNoteFromTrash(note.filename, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
833
- const restoredNote = fileReader.readNoteFile(preview.fromFilename);
950
+ const preview = await fileWriter.previewRestoreLocalNoteFromTrash(note.filename, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
951
+ const restoredNote = await fileReader.readNoteFile(preview.fromFilename);
834
952
  if (!restoredNote) {
835
953
  throw new Error('Failed to read local trash note');
836
954
  }
@@ -841,11 +959,11 @@ export function previewRestoreNote(identifier, destinationFolder) {
841
959
  toIdentifier: preview.toFilename,
842
960
  };
843
961
  }
844
- export function restoreNote(identifier, destinationFolder) {
845
- const preview = previewRestoreNote(identifier, destinationFolder);
962
+ export async function restoreNote(identifier, destinationFolder) {
963
+ const preview = await previewRestoreNote(identifier, destinationFolder);
846
964
  if (preview.source === 'space') {
847
965
  sqliteWriter.restoreSpaceNote(preview.fromIdentifier, preview.toIdentifier);
848
- const restoredNote = sqliteReader.getSpaceNote(preview.fromIdentifier);
966
+ const restoredNote = await sqliteReader.getSpaceNote(preview.fromIdentifier);
849
967
  if (!restoredNote) {
850
968
  throw new Error('Failed to read TeamSpace note after restore');
851
969
  }
@@ -857,8 +975,8 @@ export function restoreNote(identifier, destinationFolder) {
857
975
  toIdentifier: preview.toIdentifier,
858
976
  };
859
977
  }
860
- const restoredFilename = fileWriter.restoreLocalNoteFromTrash(preview.fromIdentifier, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
861
- const restoredNote = fileReader.readNoteFile(restoredFilename);
978
+ const restoredFilename = await fileWriter.restoreLocalNoteFromTrash(preview.fromIdentifier, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
979
+ const restoredNote = await fileReader.readNoteFile(restoredFilename);
862
980
  if (!restoredNote) {
863
981
  throw new Error('Failed to read local note after restore');
864
982
  }
@@ -870,18 +988,18 @@ export function restoreNote(identifier, destinationFolder) {
870
988
  toIdentifier: preview.toIdentifier,
871
989
  };
872
990
  }
873
- export function previewRenameNoteFile(filename, newFilename, keepExtension = true) {
874
- const note = getLocalProjectNoteOrThrow(filename, 'Rename note file');
875
- const preview = fileWriter.previewRenameLocalNoteFile(note.filename, newFilename, keepExtension);
991
+ export async function previewRenameNoteFile(filename, newFilename, keepExtension = true) {
992
+ const note = await getLocalProjectNoteOrThrow(filename, 'Rename note file');
993
+ const preview = await fileWriter.previewRenameLocalNoteFile(note.filename, newFilename, keepExtension);
876
994
  return {
877
995
  note,
878
996
  ...preview,
879
997
  };
880
998
  }
881
- export function renameNoteFile(filename, newFilename, keepExtension = true) {
882
- const preview = previewRenameNoteFile(filename, newFilename, keepExtension);
883
- const nextFilename = fileWriter.renameLocalNoteFile(filename, newFilename, keepExtension);
884
- const renamedNote = fileReader.readNoteFile(nextFilename);
999
+ export async function renameNoteFile(filename, newFilename, keepExtension = true) {
1000
+ const preview = await previewRenameNoteFile(filename, newFilename, keepExtension);
1001
+ const nextFilename = await fileWriter.renameLocalNoteFile(filename, newFilename, keepExtension);
1002
+ const renamedNote = await fileReader.readNoteFile(nextFilename);
885
1003
  if (!renamedNote) {
886
1004
  throw new Error('Failed to read note after rename');
887
1005
  }
@@ -892,8 +1010,8 @@ export function renameNoteFile(filename, newFilename, keepExtension = true) {
892
1010
  toFilename: preview.toFilename,
893
1011
  };
894
1012
  }
895
- export function renameSpaceNote(identifier, newTitle) {
896
- const note = getNoteByIdentifierOrThrow(identifier);
1013
+ export async function renameSpaceNote(identifier, newTitle) {
1014
+ const note = await getNoteByIdentifierOrThrow(identifier);
897
1015
  if (note.source !== 'space') {
898
1016
  throw new Error('renameSpaceNote is for TeamSpace notes only');
899
1017
  }
@@ -903,7 +1021,7 @@ export function renameSpaceNote(identifier, newTitle) {
903
1021
  const fromTitle = note.title;
904
1022
  const writeId = note.id || note.filename;
905
1023
  sqliteWriter.updateSpaceNoteTitle(writeId, newTitle);
906
- const renamedNote = sqliteReader.getSpaceNote(writeId);
1024
+ const renamedNote = await sqliteReader.getSpaceNote(writeId);
907
1025
  if (!renamedNote) {
908
1026
  throw new Error('Failed to read note after rename');
909
1027
  }
@@ -917,40 +1035,45 @@ export function renameSpaceNote(identifier, newTitle) {
917
1035
  /**
918
1036
  * Get today's daily note
919
1037
  */
920
- export function getTodayNote(space) {
1038
+ export async function getTodayNote(space) {
921
1039
  const dateStr = getTodayDateString();
922
1040
  return getCalendarNote(dateStr, space);
923
1041
  }
924
1042
  /**
925
1043
  * Get a calendar note by date
926
1044
  */
927
- export function getCalendarNote(date, space) {
1045
+ export async function getCalendarNote(date, space) {
928
1046
  const dateStr = parseFlexibleDate(date);
929
- const resolvedSpace = resolveSpaceId(space);
1047
+ const resolvedSpace = await resolveSpaceId(space);
930
1048
  if (resolvedSpace) {
931
- return sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1049
+ return await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
932
1050
  }
933
- return fileReader.getCalendarNote(dateStr);
1051
+ const note = await fileReader.getCalendarNote(dateStr);
1052
+ if (!note)
1053
+ return null;
1054
+ if (!isFolderAllowed(note.filename))
1055
+ return null;
1056
+ return note;
934
1057
  }
935
1058
  /**
936
1059
  * Ensure a calendar note exists, create if not
937
1060
  */
938
- export function ensureCalendarNote(date, space) {
1061
+ export async function ensureCalendarNote(date, space) {
939
1062
  const dateStr = parseFlexibleDate(date);
940
- const resolvedSpace = resolveSpaceId(space);
1063
+ const resolvedSpace = await resolveSpaceId(space);
941
1064
  if (resolvedSpace) {
942
- let note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1065
+ let note = await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
943
1066
  if (!note) {
944
1067
  sqliteWriter.createSpaceCalendarNote(resolvedSpace, dateStr, '');
945
- note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1068
+ note = await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
946
1069
  invalidateListingCaches();
947
1070
  }
948
1071
  if (!note)
949
1072
  throw new Error('Failed to create space calendar note');
950
1073
  return note;
951
1074
  }
952
- const filename = fileWriter.ensureCalendarNote(dateStr);
953
- const note = fileReader.readNoteFile(filename);
1075
+ const filename = await fileWriter.ensureCalendarNote(dateStr);
1076
+ const note = await fileReader.readNoteFile(filename);
954
1077
  if (!note)
955
1078
  throw new Error('Failed to create calendar note');
956
1079
  invalidateListingCaches();
@@ -959,8 +1082,8 @@ export function ensureCalendarNote(date, space) {
959
1082
  /**
960
1083
  * Add content to today's note
961
1084
  */
962
- export function addToToday(content, position = 'end', space) {
963
- const note = ensureCalendarNote('today', space);
1085
+ export async function addToToday(content, position = 'end', space) {
1086
+ const note = await ensureCalendarNote('today', space);
964
1087
  let newContent;
965
1088
  if (position === 'start') {
966
1089
  const lines = note.content.split('\n');
@@ -986,16 +1109,16 @@ export function addToToday(content, position = 'end', space) {
986
1109
  /**
987
1110
  * List all spaces
988
1111
  */
989
- export function listSpaces() {
990
- return sqliteReader.listSpaces();
1112
+ export async function listSpaces() {
1113
+ return await sqliteReader.listSpaces();
991
1114
  }
992
1115
  // Keep old name for backwards compatibility
993
1116
  export const listTeamspaces = listSpaces;
994
1117
  /**
995
1118
  * List folders with optional source/depth/query filtering
996
1119
  */
997
- export function listFolders(options = {}) {
998
- const resolvedSpace = resolveSpaceId(options.space);
1120
+ export async function listFolders(options = {}) {
1121
+ const resolvedSpace = await resolveSpaceId(options.space);
999
1122
  const { includeLocal = !resolvedSpace, includeSpaces = Boolean(resolvedSpace), query, maxDepth, parentPath, recursive = true, } = options;
1000
1123
  const space = resolvedSpace;
1001
1124
  const normalizedQuery = query?.trim().toLowerCase() || '';
@@ -1015,22 +1138,27 @@ export function listFolders(options = {}) {
1015
1138
  }
1016
1139
  const folders = [];
1017
1140
  if (includeLocal) {
1018
- folders.push(...fileReader.listFolders(maxDepth));
1141
+ folders.push(...(await fileReader.listFolders(maxDepth)));
1019
1142
  }
1020
1143
  if (includeSpaces) {
1021
- folders.push(...sqliteReader.listSpaceFolders(space));
1144
+ folders.push(...await sqliteReader.listSpaceFolders(space));
1022
1145
  }
1023
1146
  const deduped = folders.filter((folder, index, arr) => {
1024
1147
  const key = `${folder.source}:${folder.spaceId || ''}:${folder.path}`;
1025
1148
  return arr.findIndex((candidate) => `${candidate.source}:${candidate.spaceId || ''}:${candidate.path}` === key) === index;
1026
1149
  });
1150
+ const accessFiltered = deduped.filter((folder) => {
1151
+ if (folder.source !== 'local')
1152
+ return true;
1153
+ return isFolderAllowed(`Notes/${folder.path}`);
1154
+ });
1027
1155
  let filtered = normalizedQuery
1028
- ? deduped.filter((folder) => {
1156
+ ? accessFiltered.filter((folder) => {
1029
1157
  const path = folder.path.toLowerCase();
1030
1158
  const name = folder.name.toLowerCase();
1031
1159
  return path.includes(normalizedQuery) || name.includes(normalizedQuery);
1032
1160
  })
1033
- : deduped;
1161
+ : accessFiltered;
1034
1162
  if (normalizedParentPath) {
1035
1163
  const parentPrefix = `${normalizedParentPath}/`;
1036
1164
  filtered = filtered.filter((folder) => {
@@ -1049,21 +1177,21 @@ export function listFolders(options = {}) {
1049
1177
  filtered.sort((a, b) => a.path.localeCompare(b.path));
1050
1178
  return setCachedValue(listFoldersCache, cacheKey, filtered, LIST_FOLDERS_CACHE_TTL_MS);
1051
1179
  }
1052
- function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1180
+ async function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1053
1181
  const normalized = reference.trim();
1054
1182
  if (!normalized) {
1055
1183
  throw new Error('Folder reference is required');
1056
1184
  }
1057
1185
  const lower = normalized.toLowerCase();
1058
1186
  if (options.allowRoot === true && (lower === 'root' || lower === 'space-root' || normalized === spaceId)) {
1059
- const space = sqliteReader.listSpaces().find((candidate) => candidate.id === spaceId);
1187
+ const space = (await sqliteReader.listSpaces()).find((candidate) => candidate.id === spaceId);
1060
1188
  return {
1061
1189
  id: spaceId,
1062
1190
  path: space?.name || spaceId,
1063
1191
  name: space?.name || spaceId,
1064
1192
  };
1065
1193
  }
1066
- const folder = sqliteReader.resolveSpaceFolder(spaceId, normalized, {
1194
+ const folder = await sqliteReader.resolveSpaceFolder(spaceId, normalized, {
1067
1195
  includeTrash: options.includeTrash === true,
1068
1196
  });
1069
1197
  if (!folder?.id) {
@@ -1075,9 +1203,9 @@ function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1075
1203
  name: folder.name,
1076
1204
  };
1077
1205
  }
1078
- export function previewCreateFolder(options) {
1206
+ export async function previewCreateFolder(options) {
1079
1207
  if ('space' in options) {
1080
- const spaceId = resolveSpaceId(options.space.trim());
1208
+ const spaceId = await resolveSpaceId(options.space.trim());
1081
1209
  if (!spaceId) {
1082
1210
  throw new Error('space is required');
1083
1211
  }
@@ -1087,8 +1215,8 @@ export function previewCreateFolder(options) {
1087
1215
  }
1088
1216
  const parent = options.parent?.trim();
1089
1217
  const destination = parent
1090
- ? resolveSpaceFolderReference(spaceId, parent, { allowRoot: true, includeTrash: true })
1091
- : resolveSpaceFolderReference(spaceId, spaceId, { allowRoot: true, includeTrash: true });
1218
+ ? await resolveSpaceFolderReference(spaceId, parent, { allowRoot: true, includeTrash: true })
1219
+ : await resolveSpaceFolderReference(spaceId, spaceId, { allowRoot: true, includeTrash: true });
1092
1220
  if (destination.name.toLowerCase() === '@trash') {
1093
1221
  throw new Error('Destination parent cannot be @Trash');
1094
1222
  }
@@ -1101,21 +1229,21 @@ export function previewCreateFolder(options) {
1101
1229
  parentId: destination.id,
1102
1230
  };
1103
1231
  }
1104
- const folder = fileWriter.previewCreateFolder(options.path);
1232
+ const folder = await fileWriter.previewCreateFolder(options.path);
1105
1233
  return {
1106
1234
  source: 'local',
1107
1235
  path: folder,
1108
1236
  name: path.basename(folder),
1109
1237
  };
1110
1238
  }
1111
- export function createFolder(options) {
1112
- const preview = previewCreateFolder(options);
1239
+ export async function createFolder(options) {
1240
+ const preview = await previewCreateFolder(options);
1113
1241
  if ('space' in options) {
1114
1242
  if (preview.source !== 'space') {
1115
1243
  throw new Error('Invalid folder create state');
1116
1244
  }
1117
1245
  const createdId = sqliteWriter.createSpaceFolder(preview.spaceId, preview.name, preview.parentId);
1118
- const createdFolder = resolveSpaceFolderReference(preview.spaceId, createdId, {
1246
+ const createdFolder = await resolveSpaceFolderReference(preview.spaceId, createdId, {
1119
1247
  allowRoot: false,
1120
1248
  includeTrash: true,
1121
1249
  });
@@ -1129,7 +1257,7 @@ export function createFolder(options) {
1129
1257
  parentId: preview.parentId,
1130
1258
  };
1131
1259
  }
1132
- const createdPath = fileWriter.createFolder(options.path);
1260
+ const createdPath = await fileWriter.createFolder(options.path);
1133
1261
  invalidateListingCaches();
1134
1262
  return {
1135
1263
  source: 'local',
@@ -1137,24 +1265,24 @@ export function createFolder(options) {
1137
1265
  name: path.basename(createdPath),
1138
1266
  };
1139
1267
  }
1140
- export function previewMoveFolder(options) {
1268
+ export async function previewMoveFolder(options) {
1141
1269
  if ('space' in options) {
1142
- const spaceId = resolveSpaceId(options.space.trim());
1270
+ const spaceId = await resolveSpaceId(options.space.trim());
1143
1271
  if (!spaceId) {
1144
1272
  throw new Error('space is required');
1145
1273
  }
1146
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1274
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1147
1275
  allowRoot: false,
1148
1276
  includeTrash: true,
1149
1277
  });
1150
- const destination = resolveSpaceFolderReference(spaceId, options.destination, {
1278
+ const destination = await resolveSpaceFolderReference(spaceId, options.destination, {
1151
1279
  allowRoot: true,
1152
1280
  includeTrash: true,
1153
1281
  });
1154
1282
  if (destination.name.toLowerCase() === '@trash') {
1155
1283
  throw new Error('Destination folder cannot be @Trash');
1156
1284
  }
1157
- const counts = sqliteReader.countSpaceFolderContents(source.id);
1285
+ const counts = await sqliteReader.countSpaceFolderContents(source.id);
1158
1286
  return {
1159
1287
  source: 'space',
1160
1288
  spaceId,
@@ -1166,9 +1294,9 @@ export function previewMoveFolder(options) {
1166
1294
  affectedFolderCount: counts.folderCount,
1167
1295
  };
1168
1296
  }
1169
- const preview = fileWriter.previewMoveLocalFolder(options.sourcePath, options.destinationFolder);
1297
+ const preview = await fileWriter.previewMoveLocalFolder(options.sourcePath, options.destinationFolder);
1170
1298
  const fullPath = path.join(fileReader.getNotesPath(), preview.fromFolder);
1171
- const counts = fileReader.countNotesInDirectory(fullPath);
1299
+ const counts = await fileReader.countNotesInDirectory(fullPath);
1172
1300
  return {
1173
1301
  source: 'local',
1174
1302
  fromPath: preview.fromFolder,
@@ -1178,14 +1306,14 @@ export function previewMoveFolder(options) {
1178
1306
  affectedFolderCount: counts.folderCount,
1179
1307
  };
1180
1308
  }
1181
- export function moveFolder(options) {
1182
- const preview = previewMoveFolder(options);
1309
+ export async function moveFolder(options) {
1310
+ const preview = await previewMoveFolder(options);
1183
1311
  if ('space' in options) {
1184
1312
  if (preview.source !== 'space') {
1185
1313
  throw new Error('Invalid folder move state');
1186
1314
  }
1187
1315
  sqliteWriter.moveSpaceFolder(preview.folderId, preview.destinationParentId);
1188
- const moved = resolveSpaceFolderReference(preview.spaceId, preview.folderId, {
1316
+ const moved = await resolveSpaceFolderReference(preview.spaceId, preview.folderId, {
1189
1317
  allowRoot: false,
1190
1318
  includeTrash: true,
1191
1319
  });
@@ -1195,7 +1323,7 @@ export function moveFolder(options) {
1195
1323
  toPath: moved.path,
1196
1324
  };
1197
1325
  }
1198
- const moved = fileWriter.moveLocalFolder(options.sourcePath, options.destinationFolder);
1326
+ const moved = await fileWriter.moveLocalFolder(options.sourcePath, options.destinationFolder);
1199
1327
  invalidateListingCaches();
1200
1328
  return {
1201
1329
  source: 'local',
@@ -1204,17 +1332,17 @@ export function moveFolder(options) {
1204
1332
  destinationFolder: moved.destinationFolder || options.destinationFolder,
1205
1333
  };
1206
1334
  }
1207
- export function previewDeleteFolder(options) {
1335
+ export async function previewDeleteFolder(options) {
1208
1336
  if ('space' in options) {
1209
- const spaceId = resolveSpaceId(options.space.trim());
1337
+ const spaceId = await resolveSpaceId(options.space.trim());
1210
1338
  if (!spaceId) {
1211
1339
  throw new Error('space is required');
1212
1340
  }
1213
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1341
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1214
1342
  allowRoot: false,
1215
1343
  includeTrash: true,
1216
1344
  });
1217
- const counts = sqliteReader.countSpaceFolderContents(source.id);
1345
+ const counts = await sqliteReader.countSpaceFolderContents(source.id);
1218
1346
  return {
1219
1347
  source: 'space',
1220
1348
  spaceId,
@@ -1225,9 +1353,9 @@ export function previewDeleteFolder(options) {
1225
1353
  affectedFolderCount: counts.folderCount,
1226
1354
  };
1227
1355
  }
1228
- const normalized = fileWriter.previewDeleteLocalFolder(options.path);
1356
+ const normalized = await fileWriter.previewDeleteLocalFolder(options.path);
1229
1357
  const fullPath = path.join(fileReader.getNotesPath(), normalized);
1230
- const counts = fileReader.countNotesInDirectory(fullPath);
1358
+ const counts = await fileReader.countNotesInDirectory(fullPath);
1231
1359
  return {
1232
1360
  source: 'local',
1233
1361
  fromPath: normalized,
@@ -1236,8 +1364,8 @@ export function previewDeleteFolder(options) {
1236
1364
  affectedFolderCount: counts.folderCount,
1237
1365
  };
1238
1366
  }
1239
- export function deleteFolder(options) {
1240
- const preview = previewDeleteFolder(options);
1367
+ export async function deleteFolder(options) {
1368
+ const preview = await previewDeleteFolder(options);
1241
1369
  if ('space' in options) {
1242
1370
  if (preview.source !== 'space') {
1243
1371
  throw new Error('Invalid folder delete state');
@@ -1252,7 +1380,7 @@ export function deleteFolder(options) {
1252
1380
  trashFolderId: deleted.trashFolderId,
1253
1381
  };
1254
1382
  }
1255
- const trashedPath = fileWriter.deleteLocalFolder(options.path);
1383
+ const trashedPath = await fileWriter.deleteLocalFolder(options.path);
1256
1384
  invalidateListingCaches();
1257
1385
  return {
1258
1386
  source: 'local',
@@ -1260,13 +1388,13 @@ export function deleteFolder(options) {
1260
1388
  trashedPath,
1261
1389
  };
1262
1390
  }
1263
- export function previewRenameFolder(options) {
1391
+ export async function previewRenameFolder(options) {
1264
1392
  if ('space' in options) {
1265
- const spaceId = resolveSpaceId(options.space.trim());
1393
+ const spaceId = await resolveSpaceId(options.space.trim());
1266
1394
  if (!spaceId) {
1267
1395
  throw new Error('space is required');
1268
1396
  }
1269
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1397
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1270
1398
  allowRoot: false,
1271
1399
  includeTrash: true,
1272
1400
  });
@@ -1286,21 +1414,21 @@ export function previewRenameFolder(options) {
1286
1414
  name: newName,
1287
1415
  };
1288
1416
  }
1289
- const preview = fileWriter.previewRenameLocalFolder(options.sourcePath, options.newName);
1417
+ const preview = await fileWriter.previewRenameLocalFolder(options.sourcePath, options.newName);
1290
1418
  return {
1291
1419
  source: 'local',
1292
1420
  fromPath: preview.fromFolder,
1293
1421
  toPath: preview.toFolder,
1294
1422
  };
1295
1423
  }
1296
- export function renameFolder(options) {
1297
- const preview = previewRenameFolder(options);
1424
+ export async function renameFolder(options) {
1425
+ const preview = await previewRenameFolder(options);
1298
1426
  if ('space' in options) {
1299
1427
  if (preview.source !== 'space') {
1300
1428
  throw new Error('Invalid folder rename state');
1301
1429
  }
1302
1430
  const renamed = sqliteWriter.renameSpaceFolder(preview.folderId, preview.name);
1303
- const folder = resolveSpaceFolderReference(preview.spaceId, renamed.folderId, {
1431
+ const folder = await resolveSpaceFolderReference(preview.spaceId, renamed.folderId, {
1304
1432
  allowRoot: false,
1305
1433
  includeTrash: true,
1306
1434
  });
@@ -1310,7 +1438,7 @@ export function renameFolder(options) {
1310
1438
  toPath: folder.path,
1311
1439
  };
1312
1440
  }
1313
- const renamed = fileWriter.renameLocalFolder(options.sourcePath, options.newName);
1441
+ const renamed = await fileWriter.renameLocalFolder(options.sourcePath, options.newName);
1314
1442
  invalidateListingCaches();
1315
1443
  return {
1316
1444
  source: 'local',
@@ -1321,13 +1449,31 @@ export function renameFolder(options) {
1321
1449
  /**
1322
1450
  * List all tags
1323
1451
  */
1324
- export function listTags(space) {
1325
- const resolvedSpace = resolveSpaceId(space);
1452
+ export async function listTags(space) {
1453
+ const resolvedSpace = await resolveSpaceId(space);
1454
+ const cacheKey = resolvedSpace ?? '__all__';
1455
+ const cached = getCachedValue(listTagsCache, cacheKey);
1456
+ if (cached)
1457
+ return cached;
1326
1458
  const tags = new Set();
1327
1459
  if (!resolvedSpace) {
1328
- fileReader.extractAllTags().forEach((tag) => tags.add(tag));
1460
+ if (hasFolderAccessRules()) {
1461
+ // The fast paths (bridge tags endpoint, ripgrep) scan everything on
1462
+ // disk and can't tell us which file a tag came from — so they would
1463
+ // surface tag names that exist only inside a denied folder. Fall
1464
+ // back to iterating filtered notes so the rules apply.
1465
+ const notes = await listNotes();
1466
+ for (const note of notes) {
1467
+ for (const tag of extractTagsFromContent(note.content)) {
1468
+ tags.add(tag);
1469
+ }
1470
+ }
1471
+ }
1472
+ else {
1473
+ (await fileReader.extractAllTags()).forEach((tag) => tags.add(tag));
1474
+ }
1329
1475
  }
1330
- sqliteReader.extractSpaceTags(resolvedSpace).forEach((tag) => tags.add(tag));
1331
- return Array.from(tags).sort();
1476
+ (await sqliteReader.extractSpaceTags(resolvedSpace)).forEach((tag) => tags.add(tag));
1477
+ return setCachedValue(listTagsCache, cacheKey, Array.from(tags).sort(), LIST_TAGS_CACHE_TTL_MS);
1332
1478
  }
1333
1479
  //# sourceMappingURL=unified-store.js.map