@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
@@ -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) => {
@@ -493,7 +527,7 @@ function noteToSearchResult(note, query) {
493
527
  /**
494
528
  * Convert ripgrep matches to SearchResults
495
529
  */
496
- function convertRipgrepToSearchResults(matches) {
530
+ async function convertRipgrepToSearchResults(matches) {
497
531
  // Group matches by file
498
532
  const byFile = new Map();
499
533
  for (const m of matches) {
@@ -503,7 +537,7 @@ function convertRipgrepToSearchResults(matches) {
503
537
  }
504
538
  const results = [];
505
539
  for (const [file, fileMatches] of byFile) {
506
- const note = fileReader.readNoteFile(file);
540
+ const note = await fileReader.readNoteFile(file);
507
541
  if (note) {
508
542
  results.push({
509
543
  note,
@@ -622,8 +656,38 @@ function calculateScore(note, matches, query) {
622
656
  /**
623
657
  * Create a new note with smart folder matching
624
658
  */
625
- export function createNote(title, content, options = {}) {
626
- 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
+ }
627
691
  // Initialize folder resolution info
628
692
  const folderResolution = {
629
693
  requested: folder,
@@ -654,10 +718,16 @@ export function createNote(title, content, options = {}) {
654
718
  }
655
719
  }
656
720
  }
657
- const resolvedSpace = resolveSpaceId(space);
721
+ const resolvedSpace = await resolveSpaceId(space);
658
722
  if (resolvedSpace) {
659
- const filename = sqliteWriter.createSpaceNote(resolvedSpace, title, effectiveContent);
660
- 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);
661
731
  if (!note)
662
732
  throw new Error('Failed to create space note');
663
733
  invalidateListingCaches();
@@ -666,7 +736,7 @@ export function createNote(title, content, options = {}) {
666
736
  // Smart folder matching for local notes
667
737
  let resolvedFolder = folder;
668
738
  if (folder && !createNewFolder) {
669
- const folders = fileReader.listFolders();
739
+ const folders = await fileReader.listFolders();
670
740
  const match = matchFolder(folder, folders);
671
741
  if (match.matched && match.folder) {
672
742
  resolvedFolder = match.folder.path;
@@ -677,8 +747,8 @@ export function createNote(title, content, options = {}) {
677
747
  folderResolution.alternatives = match.alternatives.map((f) => f.path);
678
748
  }
679
749
  }
680
- const filename = fileWriter.createProjectNote(title, effectiveContent, resolvedFolder);
681
- const note = fileReader.readNoteFile(filename);
750
+ const writtenFilename = await fileWriter.createProjectNote(title, effectiveContent, resolvedFolder, filename);
751
+ const note = await fileReader.readNoteFile(writtenFilename);
682
752
  if (!note)
683
753
  throw new Error('Failed to create note');
684
754
  invalidateListingCaches();
@@ -687,23 +757,23 @@ export function createNote(title, content, options = {}) {
687
757
  /**
688
758
  * Update a note's content
689
759
  */
690
- export function updateNote(identifier, content, options = {}) {
691
- const updateSpace = (spaceIdentifier) => {
692
- 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);
693
763
  if (!existing) {
694
764
  throw new Error(`Note not found: ${spaceIdentifier}`);
695
765
  }
696
766
  const writeIdentifier = existing.id || spaceIdentifier;
697
767
  sqliteWriter.updateSpaceNote(writeIdentifier, content);
698
- const note = sqliteReader.getSpaceNote(writeIdentifier);
768
+ const note = await sqliteReader.getSpaceNote(writeIdentifier);
699
769
  if (!note)
700
770
  throw new Error('Note not found after update');
701
771
  invalidateListingCaches();
702
772
  return note;
703
773
  };
704
- const updateLocal = (localIdentifier) => {
705
- fileWriter.updateNote(localIdentifier, content);
706
- const note = fileReader.readNoteFile(localIdentifier);
774
+ const updateLocal = async (localIdentifier) => {
775
+ await fileWriter.updateNote(localIdentifier, content);
776
+ const note = await fileReader.readNoteFile(localIdentifier);
707
777
  if (!note)
708
778
  throw new Error('Note not found after update');
709
779
  invalidateListingCaches();
@@ -718,8 +788,8 @@ export function updateNote(identifier, content, options = {}) {
718
788
  if (identifier.includes('%%NotePlanCloud%%')) {
719
789
  return updateSpace(identifier);
720
790
  }
721
- const localNote = fileReader.readNoteFile(identifier);
722
- const spaceNote = sqliteReader.getSpaceNote(identifier);
791
+ const localNote = await fileReader.readNoteFile(identifier);
792
+ const spaceNote = await sqliteReader.getSpaceNote(identifier);
723
793
  if (localNote) {
724
794
  return updateLocal(localNote.filename);
725
795
  }
@@ -731,8 +801,8 @@ export function updateNote(identifier, content, options = {}) {
731
801
  /**
732
802
  * Delete a note
733
803
  */
734
- export function deleteNote(identifier) {
735
- const note = getNoteByIdentifierOrThrow(identifier);
804
+ export async function deleteNote(identifier) {
805
+ const note = await getNoteByIdentifierOrThrow(identifier);
736
806
  if (note.source === 'space') {
737
807
  const moved = sqliteWriter.deleteSpaceNote(note.id || note.filename);
738
808
  invalidateListingCaches();
@@ -743,7 +813,7 @@ export function deleteNote(identifier) {
743
813
  noteId: moved.noteId,
744
814
  };
745
815
  }
746
- const trashedPath = fileWriter.deleteNote(note.filename);
816
+ const trashedPath = await fileWriter.deleteNote(note.filename);
747
817
  invalidateListingCaches();
748
818
  return {
749
819
  source: 'local',
@@ -751,8 +821,8 @@ export function deleteNote(identifier) {
751
821
  toIdentifier: trashedPath,
752
822
  };
753
823
  }
754
- function getNoteByIdentifierOrThrow(identifier, options = {}) {
755
- 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 }));
756
826
  if (!note) {
757
827
  throw new Error('Note not found');
758
828
  }
@@ -761,8 +831,8 @@ function getNoteByIdentifierOrThrow(identifier, options = {}) {
761
831
  }
762
832
  return note;
763
833
  }
764
- function getLocalProjectNoteOrThrow(identifier, action) {
765
- const note = getNoteByIdentifierOrThrow(identifier);
834
+ async function getLocalProjectNoteOrThrow(identifier, action) {
835
+ const note = await getNoteByIdentifierOrThrow(identifier);
766
836
  if (note.source !== 'local') {
767
837
  throw new Error(`${action} is currently supported for local notes only`);
768
838
  }
@@ -771,7 +841,7 @@ function getLocalProjectNoteOrThrow(identifier, action) {
771
841
  }
772
842
  return note;
773
843
  }
774
- function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
844
+ async function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
775
845
  const query = destinationFolder.trim();
776
846
  if (!query) {
777
847
  throw new Error('Destination folder is required');
@@ -786,7 +856,7 @@ function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
786
856
  label: note.spaceId,
787
857
  };
788
858
  }
789
- const folder = sqliteReader.resolveSpaceFolder(note.spaceId, query, { includeTrash: true });
859
+ const folder = await sqliteReader.resolveSpaceFolder(note.spaceId, query, { includeTrash: true });
790
860
  if (!folder?.id) {
791
861
  throw new Error(`Destination folder not found in space: ${destinationFolder}`);
792
862
  }
@@ -798,13 +868,13 @@ function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
798
868
  label: folder.path,
799
869
  };
800
870
  }
801
- export function previewMoveNote(identifier, destinationFolder) {
802
- const note = getNoteByIdentifierOrThrow(identifier);
871
+ export async function previewMoveNote(identifier, destinationFolder) {
872
+ const note = await getNoteByIdentifierOrThrow(identifier);
803
873
  if (note.source === 'space') {
804
874
  if (note.type !== 'note') {
805
875
  throw new Error('Moving calendar notes in TeamSpaces is not supported');
806
876
  }
807
- const destination = resolveSpaceMoveDestination(note, destinationFolder);
877
+ const destination = await resolveSpaceMoveDestination(note, destinationFolder);
808
878
  return {
809
879
  note,
810
880
  fromFilename: note.filename,
@@ -813,20 +883,20 @@ export function previewMoveNote(identifier, destinationFolder) {
813
883
  destinationParentId: destination.id,
814
884
  };
815
885
  }
816
- const preview = fileWriter.previewMoveLocalNote(note.filename, destinationFolder);
886
+ const preview = await fileWriter.previewMoveLocalNote(note.filename, destinationFolder);
817
887
  return {
818
888
  note,
819
889
  ...preview,
820
890
  };
821
891
  }
822
- export function moveNote(identifier, destinationFolder) {
823
- const preview = previewMoveNote(identifier, destinationFolder);
892
+ export async function moveNote(identifier, destinationFolder) {
893
+ const preview = await previewMoveNote(identifier, destinationFolder);
824
894
  if (preview.note.source === 'space') {
825
895
  if (!preview.note.id || !preview.destinationParentId) {
826
896
  throw new Error('Could not resolve TeamSpace move target');
827
897
  }
828
898
  sqliteWriter.moveSpaceNote(preview.note.id, preview.destinationParentId);
829
- const movedNote = sqliteReader.getSpaceNote(preview.note.id);
899
+ const movedNote = await sqliteReader.getSpaceNote(preview.note.id);
830
900
  if (!movedNote) {
831
901
  throw new Error('Failed to read note after move');
832
902
  }
@@ -839,8 +909,8 @@ export function moveNote(identifier, destinationFolder) {
839
909
  destinationParentId: preview.destinationParentId,
840
910
  };
841
911
  }
842
- const nextFilename = fileWriter.moveLocalNote(preview.note.filename, destinationFolder);
843
- const movedNote = fileReader.readNoteFile(nextFilename);
912
+ const nextFilename = await fileWriter.moveLocalNote(preview.note.filename, destinationFolder);
913
+ const movedNote = await fileReader.readNoteFile(nextFilename);
844
914
  if (!movedNote) {
845
915
  throw new Error('Failed to read note after move');
846
916
  }
@@ -852,17 +922,17 @@ export function moveNote(identifier, destinationFolder) {
852
922
  destinationFolder: preview.destinationFolder,
853
923
  };
854
924
  }
855
- export function previewRestoreNote(identifier, destinationFolder) {
856
- const note = getNoteByIdentifierOrThrow(identifier, { allowTrash: true });
925
+ export async function previewRestoreNote(identifier, destinationFolder) {
926
+ const note = await getNoteByIdentifierOrThrow(identifier, { allowTrash: true });
857
927
  if (note.source === 'space') {
858
928
  if (!note.id) {
859
929
  throw new Error('Space note ID is required for restore');
860
930
  }
861
- if (!sqliteReader.isSpaceNoteInTrash(note.id)) {
931
+ if (!await sqliteReader.isSpaceNoteInTrash(note.id)) {
862
932
  throw new Error('Note is not in TeamSpace @Trash');
863
933
  }
864
934
  const destination = destinationFolder
865
- ? resolveSpaceMoveDestination(note, destinationFolder)
935
+ ? await resolveSpaceMoveDestination(note, destinationFolder)
866
936
  : { id: note.spaceId || '', label: note.spaceId || '' };
867
937
  if (!destination.id) {
868
938
  throw new Error('Could not resolve TeamSpace restore destination');
@@ -877,8 +947,8 @@ export function previewRestoreNote(identifier, destinationFolder) {
877
947
  if (note.type !== 'trash') {
878
948
  throw new Error('Local note is not in @Trash');
879
949
  }
880
- const preview = fileWriter.previewRestoreLocalNoteFromTrash(note.filename, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
881
- 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);
882
952
  if (!restoredNote) {
883
953
  throw new Error('Failed to read local trash note');
884
954
  }
@@ -889,11 +959,11 @@ export function previewRestoreNote(identifier, destinationFolder) {
889
959
  toIdentifier: preview.toFilename,
890
960
  };
891
961
  }
892
- export function restoreNote(identifier, destinationFolder) {
893
- const preview = previewRestoreNote(identifier, destinationFolder);
962
+ export async function restoreNote(identifier, destinationFolder) {
963
+ const preview = await previewRestoreNote(identifier, destinationFolder);
894
964
  if (preview.source === 'space') {
895
965
  sqliteWriter.restoreSpaceNote(preview.fromIdentifier, preview.toIdentifier);
896
- const restoredNote = sqliteReader.getSpaceNote(preview.fromIdentifier);
966
+ const restoredNote = await sqliteReader.getSpaceNote(preview.fromIdentifier);
897
967
  if (!restoredNote) {
898
968
  throw new Error('Failed to read TeamSpace note after restore');
899
969
  }
@@ -905,8 +975,8 @@ export function restoreNote(identifier, destinationFolder) {
905
975
  toIdentifier: preview.toIdentifier,
906
976
  };
907
977
  }
908
- const restoredFilename = fileWriter.restoreLocalNoteFromTrash(preview.fromIdentifier, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
909
- 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);
910
980
  if (!restoredNote) {
911
981
  throw new Error('Failed to read local note after restore');
912
982
  }
@@ -918,18 +988,18 @@ export function restoreNote(identifier, destinationFolder) {
918
988
  toIdentifier: preview.toIdentifier,
919
989
  };
920
990
  }
921
- export function previewRenameNoteFile(filename, newFilename, keepExtension = true) {
922
- const note = getLocalProjectNoteOrThrow(filename, 'Rename note file');
923
- 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);
924
994
  return {
925
995
  note,
926
996
  ...preview,
927
997
  };
928
998
  }
929
- export function renameNoteFile(filename, newFilename, keepExtension = true) {
930
- const preview = previewRenameNoteFile(filename, newFilename, keepExtension);
931
- const nextFilename = fileWriter.renameLocalNoteFile(filename, newFilename, keepExtension);
932
- 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);
933
1003
  if (!renamedNote) {
934
1004
  throw new Error('Failed to read note after rename');
935
1005
  }
@@ -940,8 +1010,8 @@ export function renameNoteFile(filename, newFilename, keepExtension = true) {
940
1010
  toFilename: preview.toFilename,
941
1011
  };
942
1012
  }
943
- export function renameSpaceNote(identifier, newTitle) {
944
- const note = getNoteByIdentifierOrThrow(identifier);
1013
+ export async function renameSpaceNote(identifier, newTitle) {
1014
+ const note = await getNoteByIdentifierOrThrow(identifier);
945
1015
  if (note.source !== 'space') {
946
1016
  throw new Error('renameSpaceNote is for TeamSpace notes only');
947
1017
  }
@@ -951,7 +1021,7 @@ export function renameSpaceNote(identifier, newTitle) {
951
1021
  const fromTitle = note.title;
952
1022
  const writeId = note.id || note.filename;
953
1023
  sqliteWriter.updateSpaceNoteTitle(writeId, newTitle);
954
- const renamedNote = sqliteReader.getSpaceNote(writeId);
1024
+ const renamedNote = await sqliteReader.getSpaceNote(writeId);
955
1025
  if (!renamedNote) {
956
1026
  throw new Error('Failed to read note after rename');
957
1027
  }
@@ -965,40 +1035,45 @@ export function renameSpaceNote(identifier, newTitle) {
965
1035
  /**
966
1036
  * Get today's daily note
967
1037
  */
968
- export function getTodayNote(space) {
1038
+ export async function getTodayNote(space) {
969
1039
  const dateStr = getTodayDateString();
970
1040
  return getCalendarNote(dateStr, space);
971
1041
  }
972
1042
  /**
973
1043
  * Get a calendar note by date
974
1044
  */
975
- export function getCalendarNote(date, space) {
1045
+ export async function getCalendarNote(date, space) {
976
1046
  const dateStr = parseFlexibleDate(date);
977
- const resolvedSpace = resolveSpaceId(space);
1047
+ const resolvedSpace = await resolveSpaceId(space);
978
1048
  if (resolvedSpace) {
979
- return sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1049
+ return await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
980
1050
  }
981
- 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;
982
1057
  }
983
1058
  /**
984
1059
  * Ensure a calendar note exists, create if not
985
1060
  */
986
- export function ensureCalendarNote(date, space) {
1061
+ export async function ensureCalendarNote(date, space) {
987
1062
  const dateStr = parseFlexibleDate(date);
988
- const resolvedSpace = resolveSpaceId(space);
1063
+ const resolvedSpace = await resolveSpaceId(space);
989
1064
  if (resolvedSpace) {
990
- let note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1065
+ let note = await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
991
1066
  if (!note) {
992
1067
  sqliteWriter.createSpaceCalendarNote(resolvedSpace, dateStr, '');
993
- note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
1068
+ note = await sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
994
1069
  invalidateListingCaches();
995
1070
  }
996
1071
  if (!note)
997
1072
  throw new Error('Failed to create space calendar note');
998
1073
  return note;
999
1074
  }
1000
- const filename = fileWriter.ensureCalendarNote(dateStr);
1001
- const note = fileReader.readNoteFile(filename);
1075
+ const filename = await fileWriter.ensureCalendarNote(dateStr);
1076
+ const note = await fileReader.readNoteFile(filename);
1002
1077
  if (!note)
1003
1078
  throw new Error('Failed to create calendar note');
1004
1079
  invalidateListingCaches();
@@ -1007,8 +1082,8 @@ export function ensureCalendarNote(date, space) {
1007
1082
  /**
1008
1083
  * Add content to today's note
1009
1084
  */
1010
- export function addToToday(content, position = 'end', space) {
1011
- const note = ensureCalendarNote('today', space);
1085
+ export async function addToToday(content, position = 'end', space) {
1086
+ const note = await ensureCalendarNote('today', space);
1012
1087
  let newContent;
1013
1088
  if (position === 'start') {
1014
1089
  const lines = note.content.split('\n');
@@ -1034,16 +1109,16 @@ export function addToToday(content, position = 'end', space) {
1034
1109
  /**
1035
1110
  * List all spaces
1036
1111
  */
1037
- export function listSpaces() {
1038
- return sqliteReader.listSpaces();
1112
+ export async function listSpaces() {
1113
+ return await sqliteReader.listSpaces();
1039
1114
  }
1040
1115
  // Keep old name for backwards compatibility
1041
1116
  export const listTeamspaces = listSpaces;
1042
1117
  /**
1043
1118
  * List folders with optional source/depth/query filtering
1044
1119
  */
1045
- export function listFolders(options = {}) {
1046
- const resolvedSpace = resolveSpaceId(options.space);
1120
+ export async function listFolders(options = {}) {
1121
+ const resolvedSpace = await resolveSpaceId(options.space);
1047
1122
  const { includeLocal = !resolvedSpace, includeSpaces = Boolean(resolvedSpace), query, maxDepth, parentPath, recursive = true, } = options;
1048
1123
  const space = resolvedSpace;
1049
1124
  const normalizedQuery = query?.trim().toLowerCase() || '';
@@ -1063,22 +1138,27 @@ export function listFolders(options = {}) {
1063
1138
  }
1064
1139
  const folders = [];
1065
1140
  if (includeLocal) {
1066
- folders.push(...fileReader.listFolders(maxDepth));
1141
+ folders.push(...(await fileReader.listFolders(maxDepth)));
1067
1142
  }
1068
1143
  if (includeSpaces) {
1069
- folders.push(...sqliteReader.listSpaceFolders(space));
1144
+ folders.push(...await sqliteReader.listSpaceFolders(space));
1070
1145
  }
1071
1146
  const deduped = folders.filter((folder, index, arr) => {
1072
1147
  const key = `${folder.source}:${folder.spaceId || ''}:${folder.path}`;
1073
1148
  return arr.findIndex((candidate) => `${candidate.source}:${candidate.spaceId || ''}:${candidate.path}` === key) === index;
1074
1149
  });
1150
+ const accessFiltered = deduped.filter((folder) => {
1151
+ if (folder.source !== 'local')
1152
+ return true;
1153
+ return isFolderAllowed(`Notes/${folder.path}`);
1154
+ });
1075
1155
  let filtered = normalizedQuery
1076
- ? deduped.filter((folder) => {
1156
+ ? accessFiltered.filter((folder) => {
1077
1157
  const path = folder.path.toLowerCase();
1078
1158
  const name = folder.name.toLowerCase();
1079
1159
  return path.includes(normalizedQuery) || name.includes(normalizedQuery);
1080
1160
  })
1081
- : deduped;
1161
+ : accessFiltered;
1082
1162
  if (normalizedParentPath) {
1083
1163
  const parentPrefix = `${normalizedParentPath}/`;
1084
1164
  filtered = filtered.filter((folder) => {
@@ -1097,21 +1177,21 @@ export function listFolders(options = {}) {
1097
1177
  filtered.sort((a, b) => a.path.localeCompare(b.path));
1098
1178
  return setCachedValue(listFoldersCache, cacheKey, filtered, LIST_FOLDERS_CACHE_TTL_MS);
1099
1179
  }
1100
- function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1180
+ async function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1101
1181
  const normalized = reference.trim();
1102
1182
  if (!normalized) {
1103
1183
  throw new Error('Folder reference is required');
1104
1184
  }
1105
1185
  const lower = normalized.toLowerCase();
1106
1186
  if (options.allowRoot === true && (lower === 'root' || lower === 'space-root' || normalized === spaceId)) {
1107
- const space = sqliteReader.listSpaces().find((candidate) => candidate.id === spaceId);
1187
+ const space = (await sqliteReader.listSpaces()).find((candidate) => candidate.id === spaceId);
1108
1188
  return {
1109
1189
  id: spaceId,
1110
1190
  path: space?.name || spaceId,
1111
1191
  name: space?.name || spaceId,
1112
1192
  };
1113
1193
  }
1114
- const folder = sqliteReader.resolveSpaceFolder(spaceId, normalized, {
1194
+ const folder = await sqliteReader.resolveSpaceFolder(spaceId, normalized, {
1115
1195
  includeTrash: options.includeTrash === true,
1116
1196
  });
1117
1197
  if (!folder?.id) {
@@ -1123,9 +1203,9 @@ function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1123
1203
  name: folder.name,
1124
1204
  };
1125
1205
  }
1126
- export function previewCreateFolder(options) {
1206
+ export async function previewCreateFolder(options) {
1127
1207
  if ('space' in options) {
1128
- const spaceId = resolveSpaceId(options.space.trim());
1208
+ const spaceId = await resolveSpaceId(options.space.trim());
1129
1209
  if (!spaceId) {
1130
1210
  throw new Error('space is required');
1131
1211
  }
@@ -1135,8 +1215,8 @@ export function previewCreateFolder(options) {
1135
1215
  }
1136
1216
  const parent = options.parent?.trim();
1137
1217
  const destination = parent
1138
- ? resolveSpaceFolderReference(spaceId, parent, { allowRoot: true, includeTrash: true })
1139
- : 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 });
1140
1220
  if (destination.name.toLowerCase() === '@trash') {
1141
1221
  throw new Error('Destination parent cannot be @Trash');
1142
1222
  }
@@ -1149,21 +1229,21 @@ export function previewCreateFolder(options) {
1149
1229
  parentId: destination.id,
1150
1230
  };
1151
1231
  }
1152
- const folder = fileWriter.previewCreateFolder(options.path);
1232
+ const folder = await fileWriter.previewCreateFolder(options.path);
1153
1233
  return {
1154
1234
  source: 'local',
1155
1235
  path: folder,
1156
1236
  name: path.basename(folder),
1157
1237
  };
1158
1238
  }
1159
- export function createFolder(options) {
1160
- const preview = previewCreateFolder(options);
1239
+ export async function createFolder(options) {
1240
+ const preview = await previewCreateFolder(options);
1161
1241
  if ('space' in options) {
1162
1242
  if (preview.source !== 'space') {
1163
1243
  throw new Error('Invalid folder create state');
1164
1244
  }
1165
1245
  const createdId = sqliteWriter.createSpaceFolder(preview.spaceId, preview.name, preview.parentId);
1166
- const createdFolder = resolveSpaceFolderReference(preview.spaceId, createdId, {
1246
+ const createdFolder = await resolveSpaceFolderReference(preview.spaceId, createdId, {
1167
1247
  allowRoot: false,
1168
1248
  includeTrash: true,
1169
1249
  });
@@ -1177,7 +1257,7 @@ export function createFolder(options) {
1177
1257
  parentId: preview.parentId,
1178
1258
  };
1179
1259
  }
1180
- const createdPath = fileWriter.createFolder(options.path);
1260
+ const createdPath = await fileWriter.createFolder(options.path);
1181
1261
  invalidateListingCaches();
1182
1262
  return {
1183
1263
  source: 'local',
@@ -1185,24 +1265,24 @@ export function createFolder(options) {
1185
1265
  name: path.basename(createdPath),
1186
1266
  };
1187
1267
  }
1188
- export function previewMoveFolder(options) {
1268
+ export async function previewMoveFolder(options) {
1189
1269
  if ('space' in options) {
1190
- const spaceId = resolveSpaceId(options.space.trim());
1270
+ const spaceId = await resolveSpaceId(options.space.trim());
1191
1271
  if (!spaceId) {
1192
1272
  throw new Error('space is required');
1193
1273
  }
1194
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1274
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1195
1275
  allowRoot: false,
1196
1276
  includeTrash: true,
1197
1277
  });
1198
- const destination = resolveSpaceFolderReference(spaceId, options.destination, {
1278
+ const destination = await resolveSpaceFolderReference(spaceId, options.destination, {
1199
1279
  allowRoot: true,
1200
1280
  includeTrash: true,
1201
1281
  });
1202
1282
  if (destination.name.toLowerCase() === '@trash') {
1203
1283
  throw new Error('Destination folder cannot be @Trash');
1204
1284
  }
1205
- const counts = sqliteReader.countSpaceFolderContents(source.id);
1285
+ const counts = await sqliteReader.countSpaceFolderContents(source.id);
1206
1286
  return {
1207
1287
  source: 'space',
1208
1288
  spaceId,
@@ -1214,9 +1294,9 @@ export function previewMoveFolder(options) {
1214
1294
  affectedFolderCount: counts.folderCount,
1215
1295
  };
1216
1296
  }
1217
- const preview = fileWriter.previewMoveLocalFolder(options.sourcePath, options.destinationFolder);
1297
+ const preview = await fileWriter.previewMoveLocalFolder(options.sourcePath, options.destinationFolder);
1218
1298
  const fullPath = path.join(fileReader.getNotesPath(), preview.fromFolder);
1219
- const counts = fileReader.countNotesInDirectory(fullPath);
1299
+ const counts = await fileReader.countNotesInDirectory(fullPath);
1220
1300
  return {
1221
1301
  source: 'local',
1222
1302
  fromPath: preview.fromFolder,
@@ -1226,14 +1306,14 @@ export function previewMoveFolder(options) {
1226
1306
  affectedFolderCount: counts.folderCount,
1227
1307
  };
1228
1308
  }
1229
- export function moveFolder(options) {
1230
- const preview = previewMoveFolder(options);
1309
+ export async function moveFolder(options) {
1310
+ const preview = await previewMoveFolder(options);
1231
1311
  if ('space' in options) {
1232
1312
  if (preview.source !== 'space') {
1233
1313
  throw new Error('Invalid folder move state');
1234
1314
  }
1235
1315
  sqliteWriter.moveSpaceFolder(preview.folderId, preview.destinationParentId);
1236
- const moved = resolveSpaceFolderReference(preview.spaceId, preview.folderId, {
1316
+ const moved = await resolveSpaceFolderReference(preview.spaceId, preview.folderId, {
1237
1317
  allowRoot: false,
1238
1318
  includeTrash: true,
1239
1319
  });
@@ -1243,7 +1323,7 @@ export function moveFolder(options) {
1243
1323
  toPath: moved.path,
1244
1324
  };
1245
1325
  }
1246
- const moved = fileWriter.moveLocalFolder(options.sourcePath, options.destinationFolder);
1326
+ const moved = await fileWriter.moveLocalFolder(options.sourcePath, options.destinationFolder);
1247
1327
  invalidateListingCaches();
1248
1328
  return {
1249
1329
  source: 'local',
@@ -1252,17 +1332,17 @@ export function moveFolder(options) {
1252
1332
  destinationFolder: moved.destinationFolder || options.destinationFolder,
1253
1333
  };
1254
1334
  }
1255
- export function previewDeleteFolder(options) {
1335
+ export async function previewDeleteFolder(options) {
1256
1336
  if ('space' in options) {
1257
- const spaceId = resolveSpaceId(options.space.trim());
1337
+ const spaceId = await resolveSpaceId(options.space.trim());
1258
1338
  if (!spaceId) {
1259
1339
  throw new Error('space is required');
1260
1340
  }
1261
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1341
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1262
1342
  allowRoot: false,
1263
1343
  includeTrash: true,
1264
1344
  });
1265
- const counts = sqliteReader.countSpaceFolderContents(source.id);
1345
+ const counts = await sqliteReader.countSpaceFolderContents(source.id);
1266
1346
  return {
1267
1347
  source: 'space',
1268
1348
  spaceId,
@@ -1273,9 +1353,9 @@ export function previewDeleteFolder(options) {
1273
1353
  affectedFolderCount: counts.folderCount,
1274
1354
  };
1275
1355
  }
1276
- const normalized = fileWriter.previewDeleteLocalFolder(options.path);
1356
+ const normalized = await fileWriter.previewDeleteLocalFolder(options.path);
1277
1357
  const fullPath = path.join(fileReader.getNotesPath(), normalized);
1278
- const counts = fileReader.countNotesInDirectory(fullPath);
1358
+ const counts = await fileReader.countNotesInDirectory(fullPath);
1279
1359
  return {
1280
1360
  source: 'local',
1281
1361
  fromPath: normalized,
@@ -1284,8 +1364,8 @@ export function previewDeleteFolder(options) {
1284
1364
  affectedFolderCount: counts.folderCount,
1285
1365
  };
1286
1366
  }
1287
- export function deleteFolder(options) {
1288
- const preview = previewDeleteFolder(options);
1367
+ export async function deleteFolder(options) {
1368
+ const preview = await previewDeleteFolder(options);
1289
1369
  if ('space' in options) {
1290
1370
  if (preview.source !== 'space') {
1291
1371
  throw new Error('Invalid folder delete state');
@@ -1300,7 +1380,7 @@ export function deleteFolder(options) {
1300
1380
  trashFolderId: deleted.trashFolderId,
1301
1381
  };
1302
1382
  }
1303
- const trashedPath = fileWriter.deleteLocalFolder(options.path);
1383
+ const trashedPath = await fileWriter.deleteLocalFolder(options.path);
1304
1384
  invalidateListingCaches();
1305
1385
  return {
1306
1386
  source: 'local',
@@ -1308,13 +1388,13 @@ export function deleteFolder(options) {
1308
1388
  trashedPath,
1309
1389
  };
1310
1390
  }
1311
- export function previewRenameFolder(options) {
1391
+ export async function previewRenameFolder(options) {
1312
1392
  if ('space' in options) {
1313
- const spaceId = resolveSpaceId(options.space.trim());
1393
+ const spaceId = await resolveSpaceId(options.space.trim());
1314
1394
  if (!spaceId) {
1315
1395
  throw new Error('space is required');
1316
1396
  }
1317
- const source = resolveSpaceFolderReference(spaceId, options.source, {
1397
+ const source = await resolveSpaceFolderReference(spaceId, options.source, {
1318
1398
  allowRoot: false,
1319
1399
  includeTrash: true,
1320
1400
  });
@@ -1334,21 +1414,21 @@ export function previewRenameFolder(options) {
1334
1414
  name: newName,
1335
1415
  };
1336
1416
  }
1337
- const preview = fileWriter.previewRenameLocalFolder(options.sourcePath, options.newName);
1417
+ const preview = await fileWriter.previewRenameLocalFolder(options.sourcePath, options.newName);
1338
1418
  return {
1339
1419
  source: 'local',
1340
1420
  fromPath: preview.fromFolder,
1341
1421
  toPath: preview.toFolder,
1342
1422
  };
1343
1423
  }
1344
- export function renameFolder(options) {
1345
- const preview = previewRenameFolder(options);
1424
+ export async function renameFolder(options) {
1425
+ const preview = await previewRenameFolder(options);
1346
1426
  if ('space' in options) {
1347
1427
  if (preview.source !== 'space') {
1348
1428
  throw new Error('Invalid folder rename state');
1349
1429
  }
1350
1430
  const renamed = sqliteWriter.renameSpaceFolder(preview.folderId, preview.name);
1351
- const folder = resolveSpaceFolderReference(preview.spaceId, renamed.folderId, {
1431
+ const folder = await resolveSpaceFolderReference(preview.spaceId, renamed.folderId, {
1352
1432
  allowRoot: false,
1353
1433
  includeTrash: true,
1354
1434
  });
@@ -1358,7 +1438,7 @@ export function renameFolder(options) {
1358
1438
  toPath: folder.path,
1359
1439
  };
1360
1440
  }
1361
- const renamed = fileWriter.renameLocalFolder(options.sourcePath, options.newName);
1441
+ const renamed = await fileWriter.renameLocalFolder(options.sourcePath, options.newName);
1362
1442
  invalidateListingCaches();
1363
1443
  return {
1364
1444
  source: 'local',
@@ -1369,13 +1449,31 @@ export function renameFolder(options) {
1369
1449
  /**
1370
1450
  * List all tags
1371
1451
  */
1372
- export function listTags(space) {
1373
- 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;
1374
1458
  const tags = new Set();
1375
1459
  if (!resolvedSpace) {
1376
- 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
+ }
1377
1475
  }
1378
- sqliteReader.extractSpaceTags(resolvedSpace).forEach((tag) => tags.add(tag));
1379
- 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);
1380
1478
  }
1381
1479
  //# sourceMappingURL=unified-store.js.map