@noteplanco/noteplan-mcp 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +257 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +8 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/noteplan/embeddings.d.ts +170 -0
  7. package/dist/noteplan/embeddings.d.ts.map +1 -0
  8. package/dist/noteplan/embeddings.js +684 -0
  9. package/dist/noteplan/embeddings.js.map +1 -0
  10. package/dist/noteplan/file-reader.d.ts +77 -0
  11. package/dist/noteplan/file-reader.d.ts.map +1 -0
  12. package/dist/noteplan/file-reader.js +488 -0
  13. package/dist/noteplan/file-reader.js.map +1 -0
  14. package/dist/noteplan/file-writer.d.ts +108 -0
  15. package/dist/noteplan/file-writer.d.ts.map +1 -0
  16. package/dist/noteplan/file-writer.js +621 -0
  17. package/dist/noteplan/file-writer.js.map +1 -0
  18. package/dist/noteplan/filter-store.d.ts +28 -0
  19. package/dist/noteplan/filter-store.d.ts.map +1 -0
  20. package/dist/noteplan/filter-store.js +180 -0
  21. package/dist/noteplan/filter-store.js.map +1 -0
  22. package/dist/noteplan/frontmatter-parser.d.ts +45 -0
  23. package/dist/noteplan/frontmatter-parser.d.ts.map +1 -0
  24. package/dist/noteplan/frontmatter-parser.js +259 -0
  25. package/dist/noteplan/frontmatter-parser.js.map +1 -0
  26. package/dist/noteplan/fuzzy-search.d.ts +7 -0
  27. package/dist/noteplan/fuzzy-search.d.ts.map +1 -0
  28. package/dist/noteplan/fuzzy-search.js +66 -0
  29. package/dist/noteplan/fuzzy-search.js.map +1 -0
  30. package/dist/noteplan/markdown-parser.d.ts +87 -0
  31. package/dist/noteplan/markdown-parser.d.ts.map +1 -0
  32. package/dist/noteplan/markdown-parser.js +519 -0
  33. package/dist/noteplan/markdown-parser.js.map +1 -0
  34. package/dist/noteplan/preferences.d.ts +44 -0
  35. package/dist/noteplan/preferences.d.ts.map +1 -0
  36. package/dist/noteplan/preferences.js +156 -0
  37. package/dist/noteplan/preferences.js.map +1 -0
  38. package/dist/noteplan/ripgrep-search.d.ts +29 -0
  39. package/dist/noteplan/ripgrep-search.d.ts.map +1 -0
  40. package/dist/noteplan/ripgrep-search.js +110 -0
  41. package/dist/noteplan/ripgrep-search.js.map +1 -0
  42. package/dist/noteplan/sqlite-reader.d.ts +77 -0
  43. package/dist/noteplan/sqlite-reader.d.ts.map +1 -0
  44. package/dist/noteplan/sqlite-reader.js +605 -0
  45. package/dist/noteplan/sqlite-reader.js.map +1 -0
  46. package/dist/noteplan/sqlite-writer.d.ts +63 -0
  47. package/dist/noteplan/sqlite-writer.d.ts.map +1 -0
  48. package/dist/noteplan/sqlite-writer.js +574 -0
  49. package/dist/noteplan/sqlite-writer.js.map +1 -0
  50. package/dist/noteplan/types.d.ts +97 -0
  51. package/dist/noteplan/types.d.ts.map +1 -0
  52. package/dist/noteplan/types.js +33 -0
  53. package/dist/noteplan/types.js.map +1 -0
  54. package/dist/noteplan/unified-store.d.ts +289 -0
  55. package/dist/noteplan/unified-store.d.ts.map +1 -0
  56. package/dist/noteplan/unified-store.js +1308 -0
  57. package/dist/noteplan/unified-store.js.map +1 -0
  58. package/dist/server.d.ts +4 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/server.js +2468 -0
  61. package/dist/server.js.map +1 -0
  62. package/dist/tools/calendar.d.ts +311 -0
  63. package/dist/tools/calendar.d.ts.map +1 -0
  64. package/dist/tools/calendar.js +504 -0
  65. package/dist/tools/calendar.js.map +1 -0
  66. package/dist/tools/embeddings.d.ts +244 -0
  67. package/dist/tools/embeddings.d.ts.map +1 -0
  68. package/dist/tools/embeddings.js +226 -0
  69. package/dist/tools/embeddings.js.map +1 -0
  70. package/dist/tools/events.d.ts +176 -0
  71. package/dist/tools/events.d.ts.map +1 -0
  72. package/dist/tools/events.js +326 -0
  73. package/dist/tools/events.js.map +1 -0
  74. package/dist/tools/filters.d.ts +205 -0
  75. package/dist/tools/filters.d.ts.map +1 -0
  76. package/dist/tools/filters.js +347 -0
  77. package/dist/tools/filters.js.map +1 -0
  78. package/dist/tools/memory.d.ts +6 -0
  79. package/dist/tools/memory.d.ts.map +1 -0
  80. package/dist/tools/memory.js +161 -0
  81. package/dist/tools/memory.js.map +1 -0
  82. package/dist/tools/notes.d.ts +1221 -0
  83. package/dist/tools/notes.d.ts.map +1 -0
  84. package/dist/tools/notes.js +1868 -0
  85. package/dist/tools/notes.js.map +1 -0
  86. package/dist/tools/plugins.d.ts +140 -0
  87. package/dist/tools/plugins.d.ts.map +1 -0
  88. package/dist/tools/plugins.js +782 -0
  89. package/dist/tools/plugins.js.map +1 -0
  90. package/dist/tools/reminders.d.ts +207 -0
  91. package/dist/tools/reminders.d.ts.map +1 -0
  92. package/dist/tools/reminders.js +323 -0
  93. package/dist/tools/reminders.js.map +1 -0
  94. package/dist/tools/search.d.ts +58 -0
  95. package/dist/tools/search.d.ts.map +1 -0
  96. package/dist/tools/search.js +373 -0
  97. package/dist/tools/search.js.map +1 -0
  98. package/dist/tools/spaces.d.ts +484 -0
  99. package/dist/tools/spaces.d.ts.map +1 -0
  100. package/dist/tools/spaces.js +870 -0
  101. package/dist/tools/spaces.js.map +1 -0
  102. package/dist/tools/tasks.d.ts +313 -0
  103. package/dist/tools/tasks.d.ts.map +1 -0
  104. package/dist/tools/tasks.js +690 -0
  105. package/dist/tools/tasks.js.map +1 -0
  106. package/dist/tools/themes.d.ts +91 -0
  107. package/dist/tools/themes.d.ts.map +1 -0
  108. package/dist/tools/themes.js +294 -0
  109. package/dist/tools/themes.js.map +1 -0
  110. package/dist/tools/ui.d.ts +89 -0
  111. package/dist/tools/ui.d.ts.map +1 -0
  112. package/dist/tools/ui.js +137 -0
  113. package/dist/tools/ui.js.map +1 -0
  114. package/dist/utils/applescript.d.ts +5 -0
  115. package/dist/utils/applescript.d.ts.map +1 -0
  116. package/dist/utils/applescript.js +27 -0
  117. package/dist/utils/applescript.js.map +1 -0
  118. package/dist/utils/confirmation-tokens.d.ts +19 -0
  119. package/dist/utils/confirmation-tokens.d.ts.map +1 -0
  120. package/dist/utils/confirmation-tokens.js +58 -0
  121. package/dist/utils/confirmation-tokens.js.map +1 -0
  122. package/dist/utils/date-filters.d.ts +15 -0
  123. package/dist/utils/date-filters.d.ts.map +1 -0
  124. package/dist/utils/date-filters.js +129 -0
  125. package/dist/utils/date-filters.js.map +1 -0
  126. package/dist/utils/date-utils.d.ts +113 -0
  127. package/dist/utils/date-utils.d.ts.map +1 -0
  128. package/dist/utils/date-utils.js +341 -0
  129. package/dist/utils/date-utils.js.map +1 -0
  130. package/dist/utils/folder-matcher.d.ts +14 -0
  131. package/dist/utils/folder-matcher.d.ts.map +1 -0
  132. package/dist/utils/folder-matcher.js +191 -0
  133. package/dist/utils/folder-matcher.js.map +1 -0
  134. package/dist/utils/version.d.ts +10 -0
  135. package/dist/utils/version.d.ts.map +1 -0
  136. package/dist/utils/version.js +88 -0
  137. package/dist/utils/version.js.map +1 -0
  138. package/docs/plugin-api/Calendar.md +448 -0
  139. package/docs/plugin-api/CalendarItem.md +198 -0
  140. package/docs/plugin-api/Clipboard.md +101 -0
  141. package/docs/plugin-api/CommandBar.md +251 -0
  142. package/docs/plugin-api/DataStore.md +700 -0
  143. package/docs/plugin-api/Editor.md +982 -0
  144. package/docs/plugin-api/HTMLView.md +337 -0
  145. package/docs/plugin-api/NoteObject.md +588 -0
  146. package/docs/plugin-api/NotePlan.md +398 -0
  147. package/docs/plugin-api/ParagraphObject.md +242 -0
  148. package/docs/plugin-api/RangeObject.md +56 -0
  149. package/docs/plugin-api/getting-started.md +545 -0
  150. package/docs/plugin-api/plugin-api-condensed.md +526 -0
  151. package/docs/plugin-api/plugin.json +26 -0
  152. package/docs/plugin-api/script.js +542 -0
  153. package/package.json +60 -0
  154. package/scripts/calendar-helper +0 -0
  155. package/scripts/reminders-helper +0 -0
@@ -0,0 +1,1308 @@
1
+ // Unified store that merges local and space notes
2
+ import * as path from 'path';
3
+ import * as fileReader from './file-reader.js';
4
+ import * as fileWriter from './file-writer.js';
5
+ import * as sqliteReader from './sqlite-reader.js';
6
+ import * as sqliteWriter from './sqlite-writer.js';
7
+ import { parseNoteContent } from './frontmatter-parser.js';
8
+ import { getTodayDateString, parseFlexibleDate } from '../utils/date-utils.js';
9
+ import { matchFolder } from '../utils/folder-matcher.js';
10
+ import { searchWithRipgrep, isRipgrepAvailable } from './ripgrep-search.js';
11
+ import { fuzzySearch } from './fuzzy-search.js';
12
+ import { parseFlexibleDateFilter, isDateInRange } from '../utils/date-filters.js';
13
+ // Cache ripgrep availability check
14
+ let ripgrepAvailable = null;
15
+ const LIST_NOTES_CACHE_TTL_MS = 5000;
16
+ const LIST_FOLDERS_CACHE_TTL_MS = 15000;
17
+ const listNotesCache = new Map();
18
+ const listFoldersCache = new Map();
19
+ function getCachedValue(cache, key) {
20
+ const entry = cache.get(key);
21
+ if (!entry)
22
+ return undefined;
23
+ if (entry.expiresAt <= Date.now()) {
24
+ cache.delete(key);
25
+ return undefined;
26
+ }
27
+ return entry.value;
28
+ }
29
+ function setCachedValue(cache, key, value, ttlMs) {
30
+ cache.set(key, {
31
+ value,
32
+ expiresAt: Date.now() + ttlMs,
33
+ });
34
+ return value;
35
+ }
36
+ function invalidateListingCaches() {
37
+ listNotesCache.clear();
38
+ listFoldersCache.clear();
39
+ }
40
+ function normalizeLocalFolderFilter(folder) {
41
+ if (!folder)
42
+ return undefined;
43
+ let normalized = folder.trim().replace(/\\/g, '/');
44
+ if (!normalized)
45
+ return undefined;
46
+ normalized = normalized.replace(/^\/+|\/+$/g, '');
47
+ if (!normalized || normalized === 'Notes')
48
+ return undefined;
49
+ if (normalized.startsWith('Notes/')) {
50
+ normalized = normalized.slice('Notes/'.length);
51
+ }
52
+ if (!normalized || normalized === '.')
53
+ return undefined;
54
+ return normalized;
55
+ }
56
+ /**
57
+ * Resolve a space name or ID to a valid space UUID.
58
+ * Accepts either a UUID (exact match) or a human-readable name (case-insensitive).
59
+ * Returns undefined when the input is undefined/empty (pass-through for optional params).
60
+ * Throws when a non-empty value doesn't match any known space.
61
+ */
62
+ export function resolveSpaceId(space) {
63
+ if (!space)
64
+ return undefined;
65
+ const trimmed = space.trim();
66
+ if (!trimmed)
67
+ return undefined;
68
+ const spaces = sqliteReader.listSpaces();
69
+ // Exact ID match takes priority (unambiguous)
70
+ const idMatch = spaces.find((s) => s.id === trimmed);
71
+ if (idMatch)
72
+ return idMatch.id;
73
+ // Fall back to case-insensitive name match
74
+ const lower = trimmed.toLowerCase();
75
+ const nameMatches = spaces.filter((s) => s.name.toLowerCase() === lower);
76
+ if (nameMatches.length === 1)
77
+ return nameMatches[0].id;
78
+ if (nameMatches.length > 1) {
79
+ const options = nameMatches.map((s) => `${s.name} (${s.id})`).join(', ');
80
+ throw new Error(`Ambiguous space name: "${space}" matches ${nameMatches.length} spaces. Use the space ID instead: ${options}`);
81
+ }
82
+ const available = spaces.map((s) => `${s.name} (${s.id})`);
83
+ throw new Error(`Space not found: "${space}". Available spaces: ${available.length > 0 ? available.join(', ') : 'none'}`);
84
+ }
85
+ /**
86
+ * Get a note by various identifiers
87
+ */
88
+ export function getNote(options) {
89
+ const { id, title, filename, date } = options;
90
+ const space = resolveSpaceId(options.space);
91
+ // If ID is specified, get directly (best for space notes)
92
+ if (id) {
93
+ return sqliteReader.getSpaceNote(id);
94
+ }
95
+ // If date is specified, get calendar note
96
+ if (date) {
97
+ const dateStr = parseFlexibleDate(date);
98
+ if (space) {
99
+ return sqliteReader.getSpaceCalendarNote(dateStr, space);
100
+ }
101
+ return fileReader.getCalendarNote(dateStr);
102
+ }
103
+ // If filename is specified, try to get directly
104
+ if (filename) {
105
+ const normalizedFilename = filename.trim();
106
+ if (space) {
107
+ const directSpaceNote = sqliteReader.getSpaceNote(normalizedFilename);
108
+ if (directSpaceNote && directSpaceNote.spaceId === space) {
109
+ return directSpaceNote;
110
+ }
111
+ const scopedSpaceNote = sqliteReader
112
+ .listSpaceNotes(space)
113
+ .find((note) => note.filename === normalizedFilename || note.id === normalizedFilename);
114
+ if (scopedSpaceNote) {
115
+ return scopedSpaceNote;
116
+ }
117
+ }
118
+ // Check if it's a space filename
119
+ if (normalizedFilename.includes('%%NotePlanCloud%%')) {
120
+ return sqliteReader.getSpaceNote(normalizedFilename);
121
+ }
122
+ const localNote = fileReader.readNoteFile(normalizedFilename);
123
+ if (localNote)
124
+ return localNote;
125
+ return sqliteReader.getSpaceNote(normalizedFilename);
126
+ }
127
+ // If title is specified, search by title
128
+ if (title) {
129
+ if (space) {
130
+ return sqliteReader.getSpaceNoteByTitle(title, space);
131
+ }
132
+ // Try local first
133
+ const localNote = fileReader.getNoteByTitle(title);
134
+ if (localNote)
135
+ return localNote;
136
+ // Try space
137
+ return sqliteReader.getSpaceNoteByTitle(title);
138
+ }
139
+ return null;
140
+ }
141
+ /**
142
+ * List all notes, optionally filtered
143
+ */
144
+ export function listNotes(options = {}) {
145
+ const { folder, type } = options;
146
+ const space = resolveSpaceId(options.space);
147
+ const normalizedFolder = normalizeLocalFolderFilter(folder);
148
+ const cacheKey = JSON.stringify({
149
+ folder: normalizedFolder || '',
150
+ space: space || '',
151
+ type: type || '',
152
+ });
153
+ const cached = getCachedValue(listNotesCache, cacheKey);
154
+ if (cached) {
155
+ return cached;
156
+ }
157
+ const notes = [];
158
+ const hasFolderScope = Boolean(normalizedFolder);
159
+ // Get local notes
160
+ if (!space) {
161
+ if (!type || type === 'note') {
162
+ notes.push(...fileReader.listProjectNotes(normalizedFolder));
163
+ }
164
+ if ((!type || type === 'calendar') && !hasFolderScope) {
165
+ notes.push(...fileReader.listCalendarNotes());
166
+ }
167
+ }
168
+ // Get space notes
169
+ if (space || !hasFolderScope) {
170
+ notes.push(...sqliteReader.listSpaceNotes(space));
171
+ }
172
+ // Sort by modified date (newest first)
173
+ notes.sort((a, b) => {
174
+ const dateA = a.modifiedAt?.getTime() || 0;
175
+ const dateB = b.modifiedAt?.getTime() || 0;
176
+ return dateB - dateA;
177
+ });
178
+ return setCachedValue(listNotesCache, cacheKey, notes, LIST_NOTES_CACHE_TTL_MS);
179
+ }
180
+ /**
181
+ * Search across all notes with enhanced options
182
+ */
183
+ export async function searchNotes(query, options = {}) {
184
+ const { types, folder, limit = 50, fuzzy = false } = options;
185
+ const space = resolveSpaceId(options.space);
186
+ const normalizedFolder = normalizeLocalFolderFilter(folder);
187
+ const searchField = options.searchField ?? 'content';
188
+ const effectiveTypes = searchField === 'content' ? types : (types ?? ['note']);
189
+ let results = [];
190
+ let partialResults = false;
191
+ const warnings = [];
192
+ let backend = 'simple';
193
+ const lowerQuery = query.toLowerCase();
194
+ // Parse date filters
195
+ const modifiedAfter = options.modifiedAfter
196
+ ? parseFlexibleDateFilter(options.modifiedAfter)
197
+ : null;
198
+ const modifiedBefore = options.modifiedBefore
199
+ ? parseFlexibleDateFilter(options.modifiedBefore)
200
+ : null;
201
+ const createdAfter = options.createdAfter
202
+ ? parseFlexibleDateFilter(options.createdAfter)
203
+ : null;
204
+ const createdBefore = options.createdBefore
205
+ ? parseFlexibleDateFilter(options.createdBefore)
206
+ : null;
207
+ const propertyFilterEntries = Object.entries(options.propertyFilters ?? {})
208
+ .map(([key, value]) => [key.trim(), value.trim()])
209
+ .filter(([key, value]) => key.length > 0 && value.length > 0);
210
+ const propertyCaseSensitive = options.propertyCaseSensitive === true;
211
+ // Check ripgrep availability (cached)
212
+ if (ripgrepAvailable === null) {
213
+ ripgrepAvailable = await isRipgrepAvailable();
214
+ if (!ripgrepAvailable) {
215
+ console.error('Note: ripgrep not found, using fallback search for local notes');
216
+ }
217
+ }
218
+ if (searchField === 'content') {
219
+ // Search local notes
220
+ if (!space) {
221
+ if (ripgrepAvailable) {
222
+ try {
223
+ const searchPaths = normalizedFolder
224
+ ? [path.join(fileReader.getNotesPath(), normalizedFolder)]
225
+ : undefined;
226
+ const rgResult = await searchWithRipgrep(query, {
227
+ caseSensitive: options.caseSensitive,
228
+ contextLines: options.contextLines,
229
+ paths: searchPaths,
230
+ maxResults: limit * 2, // Get extra for filtering
231
+ });
232
+ backend = 'ripgrep';
233
+ partialResults = rgResult.partialResults;
234
+ if (rgResult.warning)
235
+ warnings.push(rgResult.warning);
236
+ results.push(...convertRipgrepToSearchResults(rgResult.matches));
237
+ }
238
+ catch (error) {
239
+ console.error('Ripgrep search failed:', error);
240
+ backend = 'fallback';
241
+ warnings.push('ripgrep failed; using fallback local search');
242
+ // Fall back to simple search
243
+ const localNotes = fileReader.searchLocalNotes(query, {
244
+ types: effectiveTypes,
245
+ folder: normalizedFolder,
246
+ limit: limit * 2,
247
+ });
248
+ results.push(...localNotes.map((note) => noteToSearchResult(note, lowerQuery)));
249
+ }
250
+ }
251
+ else {
252
+ backend = 'simple';
253
+ warnings.push('ripgrep unavailable; using fallback local search');
254
+ // Fallback to original search method
255
+ const localNotes = fileReader.searchLocalNotes(query, {
256
+ types: effectiveTypes,
257
+ folder: normalizedFolder,
258
+ limit: limit * 2,
259
+ });
260
+ results.push(...localNotes.map((note) => noteToSearchResult(note, lowerQuery)));
261
+ }
262
+ }
263
+ // Search space notes
264
+ const spaceNotes = sqliteReader.searchSpaceNotesFTS(query, { spaceId: space, limit: limit * 2 });
265
+ for (const note of spaceNotes) {
266
+ results.push({
267
+ note,
268
+ matches: findMatches(note.content, lowerQuery),
269
+ score: 50, // Base score, will be enhanced below
270
+ });
271
+ }
272
+ }
273
+ else {
274
+ backend = 'simple';
275
+ warnings.push(`searchField=${searchField} performs metadata matching on titles/filenames (not full-text content search).`);
276
+ const candidates = listNotes({
277
+ folder: normalizedFolder,
278
+ space,
279
+ }).filter((note) => !effectiveTypes || effectiveTypes.includes(note.type));
280
+ results = candidates
281
+ .map((note) => scoreMetadataMatch(note, query, searchField, options.caseSensitive === true))
282
+ .filter((entry) => entry !== null);
283
+ }
284
+ // Apply date filters
285
+ if (modifiedAfter || modifiedBefore || createdAfter || createdBefore) {
286
+ results = results.filter((r) => {
287
+ const modifiedOk = isDateInRange(r.note.modifiedAt, modifiedAfter, modifiedBefore);
288
+ const createdOk = isDateInRange(r.note.createdAt, createdAfter, createdBefore);
289
+ // If both filter types specified, both must pass; if only one, just that one
290
+ if ((modifiedAfter || modifiedBefore) && (createdAfter || createdBefore)) {
291
+ return modifiedOk && createdOk;
292
+ }
293
+ if (modifiedAfter || modifiedBefore)
294
+ return modifiedOk;
295
+ if (createdAfter || createdBefore)
296
+ return createdOk;
297
+ return true;
298
+ });
299
+ }
300
+ if (propertyFilterEntries.length > 0) {
301
+ results = results.filter((r) => matchesFrontmatterProperties(r.note, propertyFilterEntries, propertyCaseSensitive));
302
+ }
303
+ // Apply enhanced scoring with recency boost
304
+ results = results.map((r) => ({
305
+ ...r,
306
+ score: calculateEnhancedScore(r, lowerQuery),
307
+ }));
308
+ // Apply fuzzy re-ranking if enabled
309
+ if (fuzzy && results.length > 0) {
310
+ const allNotes = results.map((r) => r.note);
311
+ return {
312
+ results: fuzzySearch(allNotes, query, limit),
313
+ partialResults,
314
+ backend,
315
+ warnings,
316
+ };
317
+ }
318
+ // Sort by score and limit
319
+ results.sort((a, b) => b.score - a.score);
320
+ return {
321
+ results: results.slice(0, limit),
322
+ partialResults,
323
+ backend,
324
+ warnings,
325
+ };
326
+ }
327
+ function splitSearchTerms(query) {
328
+ const tokens = query
329
+ .split('|')
330
+ .map((token) => token.trim())
331
+ .filter(Boolean);
332
+ return tokens.length > 0 ? tokens : [query.trim()];
333
+ }
334
+ function metadataScore(value, term) {
335
+ if (!value || !term)
336
+ return 0;
337
+ if (value === term)
338
+ return 120;
339
+ if (value.startsWith(term))
340
+ return 100;
341
+ const slashSegment = value.split('/').some((segment) => segment === term);
342
+ if (slashSegment)
343
+ return 95;
344
+ if (value.includes(term))
345
+ return 80;
346
+ return 0;
347
+ }
348
+ function scoreMetadataMatch(note, rawQuery, searchField, caseSensitive) {
349
+ const terms = splitSearchTerms(rawQuery);
350
+ const title = caseSensitive ? note.title : note.title.toLowerCase();
351
+ const filename = caseSensitive ? note.filename : note.filename.toLowerCase();
352
+ let bestScore = 0;
353
+ let matchedOn = null;
354
+ for (const rawTerm of terms) {
355
+ const term = caseSensitive ? rawTerm : rawTerm.toLowerCase();
356
+ if (!term)
357
+ continue;
358
+ if (searchField === 'title' || searchField === 'title_or_filename') {
359
+ const score = metadataScore(title, term);
360
+ if (score > bestScore) {
361
+ bestScore = score;
362
+ matchedOn = 'title';
363
+ }
364
+ }
365
+ if (searchField === 'filename' || searchField === 'title_or_filename') {
366
+ const score = metadataScore(filename, term);
367
+ if (score > bestScore) {
368
+ bestScore = score;
369
+ matchedOn = 'filename';
370
+ }
371
+ }
372
+ }
373
+ if (bestScore <= 0 || !matchedOn)
374
+ return null;
375
+ const lineContent = matchedOn === 'title' ? note.title : note.filename;
376
+ return {
377
+ note,
378
+ matches: [
379
+ {
380
+ lineNumber: 1,
381
+ lineContent,
382
+ matchStart: 0,
383
+ matchEnd: Math.min(lineContent.length, 120),
384
+ },
385
+ ],
386
+ score: bestScore,
387
+ };
388
+ }
389
+ function normalizeFrontmatterScalar(value, caseSensitive) {
390
+ let normalized = value.trim();
391
+ const quoted = (normalized.startsWith('"') && normalized.endsWith('"')) ||
392
+ (normalized.startsWith("'") && normalized.endsWith("'"));
393
+ if (quoted && normalized.length >= 2) {
394
+ normalized = normalized.slice(1, -1).trim();
395
+ }
396
+ return caseSensitive ? normalized : normalized.toLowerCase();
397
+ }
398
+ function matchesFrontmatterProperties(note, propertyFilters, caseSensitive) {
399
+ const parsed = parseNoteContent(note.content);
400
+ if (!parsed.frontmatter)
401
+ return false;
402
+ const frontmatterEntries = Object.entries(parsed.frontmatter);
403
+ for (const [filterKey, filterValue] of propertyFilters) {
404
+ const expectedKey = caseSensitive ? filterKey : filterKey.toLowerCase();
405
+ const expectedValue = normalizeFrontmatterScalar(filterValue, caseSensitive);
406
+ const actualEntry = frontmatterEntries.find(([actualKey]) => {
407
+ const normalizedActualKey = caseSensitive ? actualKey : actualKey.toLowerCase();
408
+ return normalizedActualKey === expectedKey;
409
+ });
410
+ if (!actualEntry)
411
+ return false;
412
+ const actualValue = normalizeFrontmatterScalar(actualEntry[1], caseSensitive);
413
+ if (actualValue === expectedValue)
414
+ continue;
415
+ const listTokens = actualValue
416
+ .split(/[;,]/)
417
+ .map((token) => token.trim())
418
+ .filter(Boolean);
419
+ if (!listTokens.includes(expectedValue)) {
420
+ return false;
421
+ }
422
+ }
423
+ return true;
424
+ }
425
+ /**
426
+ * Convert a note to a SearchResult
427
+ */
428
+ function noteToSearchResult(note, query) {
429
+ const matches = findMatches(note.content, query);
430
+ return {
431
+ note,
432
+ matches,
433
+ score: calculateScore(note, matches, query),
434
+ };
435
+ }
436
+ /**
437
+ * Convert ripgrep matches to SearchResults
438
+ */
439
+ function convertRipgrepToSearchResults(matches) {
440
+ // Group matches by file
441
+ const byFile = new Map();
442
+ for (const m of matches) {
443
+ const existing = byFile.get(m.file) || [];
444
+ existing.push(m);
445
+ byFile.set(m.file, existing);
446
+ }
447
+ const results = [];
448
+ for (const [file, fileMatches] of byFile) {
449
+ const note = fileReader.readNoteFile(file);
450
+ if (note) {
451
+ results.push({
452
+ note,
453
+ matches: fileMatches.map((m) => ({
454
+ lineNumber: m.line,
455
+ lineContent: m.content,
456
+ matchStart: m.matchStart,
457
+ matchEnd: m.matchEnd,
458
+ })),
459
+ score: fileMatches.length * 10, // Score by match count
460
+ });
461
+ }
462
+ }
463
+ return results;
464
+ }
465
+ /**
466
+ * Enhanced scoring with recency boost
467
+ */
468
+ function calculateEnhancedScore(result, query) {
469
+ let score = result.matches.length; // Base: match count
470
+ const note = result.note;
471
+ // Title match bonuses
472
+ const lowerTitle = note.title.toLowerCase();
473
+ if (lowerTitle === query) {
474
+ score += 30; // Exact title match
475
+ }
476
+ else if (lowerTitle.includes(query)) {
477
+ score += 15; // Partial title match
478
+ }
479
+ // Recency bonus (modified date) - significant boost for recent notes
480
+ if (note.modifiedAt) {
481
+ const daysSinceModified = (Date.now() - note.modifiedAt.getTime()) / (1000 * 60 * 60 * 24);
482
+ if (daysSinceModified < 1) {
483
+ score += 20; // Today
484
+ }
485
+ else if (daysSinceModified < 7) {
486
+ score += 15; // This week
487
+ }
488
+ else if (daysSinceModified < 30) {
489
+ score += 8; // This month
490
+ }
491
+ else if (daysSinceModified < 90) {
492
+ score += 3; // Last 3 months
493
+ }
494
+ // Older notes get no boost
495
+ }
496
+ // Creation date bonus (smaller, for "newer" content)
497
+ if (note.createdAt) {
498
+ const daysSinceCreated = (Date.now() - note.createdAt.getTime()) / (1000 * 60 * 60 * 24);
499
+ if (daysSinceCreated < 7) {
500
+ score += 5; // Recently created
501
+ }
502
+ else if (daysSinceCreated < 30) {
503
+ score += 2;
504
+ }
505
+ }
506
+ // Penalty for @Archive and @Trash folders (push to bottom of results)
507
+ if (note.folder) {
508
+ const folderLower = note.folder.toLowerCase();
509
+ if (folderLower.includes('@archive') || folderLower.includes('@trash')) {
510
+ score -= 50; // Significant penalty to push these to the bottom
511
+ }
512
+ }
513
+ // Also check note type for trash
514
+ if (note.type === 'trash') {
515
+ score -= 50;
516
+ }
517
+ return score;
518
+ }
519
+ /**
520
+ * Find matches in content
521
+ */
522
+ function findMatches(content, query) {
523
+ const matches = [];
524
+ const lines = content.split('\n');
525
+ const lowerQuery = query.toLowerCase();
526
+ for (let i = 0; i < lines.length; i++) {
527
+ const line = lines[i];
528
+ const lowerLine = line.toLowerCase();
529
+ let index = lowerLine.indexOf(lowerQuery);
530
+ while (index !== -1) {
531
+ matches.push({
532
+ lineNumber: i + 1,
533
+ lineContent: line,
534
+ matchStart: index,
535
+ matchEnd: index + query.length,
536
+ });
537
+ index = lowerLine.indexOf(lowerQuery, index + 1);
538
+ }
539
+ }
540
+ return matches;
541
+ }
542
+ /**
543
+ * Calculate relevance score
544
+ */
545
+ function calculateScore(note, matches, query) {
546
+ let score = matches.length;
547
+ // Boost for title match
548
+ if (note.title.toLowerCase().includes(query)) {
549
+ score += 10;
550
+ }
551
+ // Boost for exact title match
552
+ if (note.title.toLowerCase() === query) {
553
+ score += 20;
554
+ }
555
+ // Boost for recent notes
556
+ if (note.modifiedAt) {
557
+ const daysSinceModified = (Date.now() - note.modifiedAt.getTime()) / (1000 * 60 * 60 * 24);
558
+ if (daysSinceModified < 7)
559
+ score += 5;
560
+ else if (daysSinceModified < 30)
561
+ score += 2;
562
+ }
563
+ return score;
564
+ }
565
+ /**
566
+ * Create a new note with smart folder matching
567
+ */
568
+ export function createNote(title, content, options = {}) {
569
+ const { folder, space, createNewFolder = false } = options;
570
+ // Initialize folder resolution info
571
+ const folderResolution = {
572
+ requested: folder,
573
+ resolved: folder,
574
+ matched: false,
575
+ ambiguous: false,
576
+ score: 0,
577
+ alternatives: [],
578
+ };
579
+ // Auto-prepend title heading if content doesn't already start with one
580
+ let effectiveContent = content || `# ${title}\n\n`;
581
+ if (content && !/^\s*(---[\s\S]*?---\s*)?#\s/.test(content)) {
582
+ effectiveContent = `# ${title}\n${content}`;
583
+ }
584
+ const resolvedSpace = resolveSpaceId(space);
585
+ if (resolvedSpace) {
586
+ const filename = sqliteWriter.createSpaceNote(resolvedSpace, title, effectiveContent);
587
+ const note = sqliteReader.getSpaceNote(filename);
588
+ if (!note)
589
+ throw new Error('Failed to create space note');
590
+ invalidateListingCaches();
591
+ return { note, folderResolution };
592
+ }
593
+ // Smart folder matching for local notes
594
+ let resolvedFolder = folder;
595
+ if (folder && !createNewFolder) {
596
+ const folders = fileReader.listFolders();
597
+ const match = matchFolder(folder, folders);
598
+ if (match.matched && match.folder) {
599
+ resolvedFolder = match.folder.path;
600
+ folderResolution.resolved = match.folder.path;
601
+ folderResolution.matched = true;
602
+ folderResolution.ambiguous = match.ambiguous;
603
+ folderResolution.score = match.score;
604
+ folderResolution.alternatives = match.alternatives.map((f) => f.path);
605
+ }
606
+ }
607
+ const filename = fileWriter.createProjectNote(title, effectiveContent, resolvedFolder);
608
+ const note = fileReader.readNoteFile(filename);
609
+ if (!note)
610
+ throw new Error('Failed to create note');
611
+ invalidateListingCaches();
612
+ return { note, folderResolution };
613
+ }
614
+ /**
615
+ * Update a note's content
616
+ */
617
+ export function updateNote(identifier, content, options = {}) {
618
+ const updateSpace = (spaceIdentifier) => {
619
+ const existing = sqliteReader.getSpaceNote(spaceIdentifier);
620
+ if (!existing) {
621
+ throw new Error(`Note not found: ${spaceIdentifier}`);
622
+ }
623
+ const writeIdentifier = existing.id || spaceIdentifier;
624
+ sqliteWriter.updateSpaceNote(writeIdentifier, content);
625
+ const note = sqliteReader.getSpaceNote(writeIdentifier);
626
+ if (!note)
627
+ throw new Error('Note not found after update');
628
+ invalidateListingCaches();
629
+ return note;
630
+ };
631
+ const updateLocal = (localIdentifier) => {
632
+ fileWriter.updateNote(localIdentifier, content);
633
+ const note = fileReader.readNoteFile(localIdentifier);
634
+ if (!note)
635
+ throw new Error('Note not found after update');
636
+ invalidateListingCaches();
637
+ return note;
638
+ };
639
+ if (options.source === 'space') {
640
+ return updateSpace(identifier);
641
+ }
642
+ if (options.source === 'local') {
643
+ return updateLocal(identifier);
644
+ }
645
+ if (identifier.includes('%%NotePlanCloud%%')) {
646
+ return updateSpace(identifier);
647
+ }
648
+ const localNote = fileReader.readNoteFile(identifier);
649
+ const spaceNote = sqliteReader.getSpaceNote(identifier);
650
+ if (localNote) {
651
+ return updateLocal(localNote.filename);
652
+ }
653
+ if (spaceNote) {
654
+ return updateSpace(spaceNote.id || identifier);
655
+ }
656
+ throw new Error(`Note not found: ${identifier}`);
657
+ }
658
+ /**
659
+ * Delete a note
660
+ */
661
+ export function deleteNote(identifier) {
662
+ const note = getNoteByIdentifierOrThrow(identifier);
663
+ if (note.source === 'space') {
664
+ const moved = sqliteWriter.deleteSpaceNote(note.id || note.filename);
665
+ invalidateListingCaches();
666
+ return {
667
+ source: 'space',
668
+ fromIdentifier: moved.noteId,
669
+ toIdentifier: moved.trashFolderId,
670
+ noteId: moved.noteId,
671
+ };
672
+ }
673
+ const trashedPath = fileWriter.deleteNote(note.filename);
674
+ invalidateListingCaches();
675
+ return {
676
+ source: 'local',
677
+ fromIdentifier: note.filename,
678
+ toIdentifier: trashedPath,
679
+ };
680
+ }
681
+ function getNoteByIdentifierOrThrow(identifier, options = {}) {
682
+ const note = getNote({ id: identifier }) ?? getNote({ filename: identifier });
683
+ if (!note) {
684
+ throw new Error('Note not found');
685
+ }
686
+ if (options.allowTrash !== true && note.type === 'trash') {
687
+ throw new Error('Note is in trash');
688
+ }
689
+ return note;
690
+ }
691
+ function getLocalProjectNoteOrThrow(identifier, action) {
692
+ const note = getNoteByIdentifierOrThrow(identifier);
693
+ if (note.source !== 'local') {
694
+ throw new Error(`${action} is currently supported for local notes only`);
695
+ }
696
+ if (note.type !== 'note') {
697
+ throw new Error(`${action} is supported for project notes only`);
698
+ }
699
+ return note;
700
+ }
701
+ function resolveSpaceMoveDestination(note, destinationFolder, options = {}) {
702
+ const query = destinationFolder.trim();
703
+ if (!query) {
704
+ throw new Error('Destination folder is required');
705
+ }
706
+ if (!note.spaceId) {
707
+ throw new Error('Could not resolve note space');
708
+ }
709
+ const normalized = query.toLowerCase();
710
+ if (normalized === 'root' || normalized === 'space-root' || query === note.spaceId) {
711
+ return {
712
+ id: note.spaceId,
713
+ label: note.spaceId,
714
+ };
715
+ }
716
+ const folder = sqliteReader.resolveSpaceFolder(note.spaceId, query, { includeTrash: true });
717
+ if (!folder?.id) {
718
+ throw new Error(`Destination folder not found in space: ${destinationFolder}`);
719
+ }
720
+ if (options.allowTrashDestination !== true && folder.name.toLowerCase() === '@trash') {
721
+ throw new Error('Use noteplan_delete_note to move notes into TeamSpace @Trash');
722
+ }
723
+ return {
724
+ id: folder.id,
725
+ label: folder.path,
726
+ };
727
+ }
728
+ export function previewMoveNote(identifier, destinationFolder) {
729
+ const note = getNoteByIdentifierOrThrow(identifier);
730
+ if (note.source === 'space') {
731
+ if (note.type !== 'note') {
732
+ throw new Error('Moving calendar notes in TeamSpaces is not supported');
733
+ }
734
+ const destination = resolveSpaceMoveDestination(note, destinationFolder);
735
+ return {
736
+ note,
737
+ fromFilename: note.filename,
738
+ toFilename: note.filename,
739
+ destinationFolder: destination.label,
740
+ destinationParentId: destination.id,
741
+ };
742
+ }
743
+ const preview = fileWriter.previewMoveLocalNote(note.filename, destinationFolder);
744
+ return {
745
+ note,
746
+ ...preview,
747
+ };
748
+ }
749
+ export function moveNote(identifier, destinationFolder) {
750
+ const preview = previewMoveNote(identifier, destinationFolder);
751
+ if (preview.note.source === 'space') {
752
+ if (!preview.note.id || !preview.destinationParentId) {
753
+ throw new Error('Could not resolve TeamSpace move target');
754
+ }
755
+ sqliteWriter.moveSpaceNote(preview.note.id, preview.destinationParentId);
756
+ const movedNote = sqliteReader.getSpaceNote(preview.note.id);
757
+ if (!movedNote) {
758
+ throw new Error('Failed to read note after move');
759
+ }
760
+ invalidateListingCaches();
761
+ return {
762
+ note: movedNote,
763
+ fromFilename: preview.fromFilename,
764
+ toFilename: preview.toFilename,
765
+ destinationFolder: preview.destinationFolder,
766
+ destinationParentId: preview.destinationParentId,
767
+ };
768
+ }
769
+ const nextFilename = fileWriter.moveLocalNote(preview.note.filename, destinationFolder);
770
+ const movedNote = fileReader.readNoteFile(nextFilename);
771
+ if (!movedNote) {
772
+ throw new Error('Failed to read note after move');
773
+ }
774
+ invalidateListingCaches();
775
+ return {
776
+ note: movedNote,
777
+ fromFilename: preview.fromFilename,
778
+ toFilename: preview.toFilename,
779
+ destinationFolder: preview.destinationFolder,
780
+ };
781
+ }
782
+ export function previewRestoreNote(identifier, destinationFolder) {
783
+ const note = getNoteByIdentifierOrThrow(identifier, { allowTrash: true });
784
+ if (note.source === 'space') {
785
+ if (!note.id) {
786
+ throw new Error('Space note ID is required for restore');
787
+ }
788
+ if (!sqliteReader.isSpaceNoteInTrash(note.id)) {
789
+ throw new Error('Note is not in TeamSpace @Trash');
790
+ }
791
+ const destination = destinationFolder
792
+ ? resolveSpaceMoveDestination(note, destinationFolder)
793
+ : { id: note.spaceId || '', label: note.spaceId || '' };
794
+ if (!destination.id) {
795
+ throw new Error('Could not resolve TeamSpace restore destination');
796
+ }
797
+ return {
798
+ source: 'space',
799
+ note,
800
+ fromIdentifier: note.id,
801
+ toIdentifier: destination.id,
802
+ };
803
+ }
804
+ if (note.type !== 'trash') {
805
+ throw new Error('Local note is not in @Trash');
806
+ }
807
+ const preview = fileWriter.previewRestoreLocalNoteFromTrash(note.filename, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
808
+ const restoredNote = fileReader.readNoteFile(preview.fromFilename);
809
+ if (!restoredNote) {
810
+ throw new Error('Failed to read local trash note');
811
+ }
812
+ return {
813
+ source: 'local',
814
+ note: restoredNote,
815
+ fromIdentifier: preview.fromFilename,
816
+ toIdentifier: preview.toFilename,
817
+ };
818
+ }
819
+ export function restoreNote(identifier, destinationFolder) {
820
+ const preview = previewRestoreNote(identifier, destinationFolder);
821
+ if (preview.source === 'space') {
822
+ sqliteWriter.restoreSpaceNote(preview.fromIdentifier, preview.toIdentifier);
823
+ const restoredNote = sqliteReader.getSpaceNote(preview.fromIdentifier);
824
+ if (!restoredNote) {
825
+ throw new Error('Failed to read TeamSpace note after restore');
826
+ }
827
+ invalidateListingCaches();
828
+ return {
829
+ source: 'space',
830
+ note: restoredNote,
831
+ fromIdentifier: preview.fromIdentifier,
832
+ toIdentifier: preview.toIdentifier,
833
+ };
834
+ }
835
+ const restoredFilename = fileWriter.restoreLocalNoteFromTrash(preview.fromIdentifier, destinationFolder && destinationFolder.trim().length > 0 ? destinationFolder : 'Notes');
836
+ const restoredNote = fileReader.readNoteFile(restoredFilename);
837
+ if (!restoredNote) {
838
+ throw new Error('Failed to read local note after restore');
839
+ }
840
+ invalidateListingCaches();
841
+ return {
842
+ source: 'local',
843
+ note: restoredNote,
844
+ fromIdentifier: preview.fromIdentifier,
845
+ toIdentifier: preview.toIdentifier,
846
+ };
847
+ }
848
+ export function previewRenameNoteFile(filename, newFilename, keepExtension = true) {
849
+ const note = getLocalProjectNoteOrThrow(filename, 'Rename note file');
850
+ const preview = fileWriter.previewRenameLocalNoteFile(note.filename, newFilename, keepExtension);
851
+ return {
852
+ note,
853
+ ...preview,
854
+ };
855
+ }
856
+ export function renameNoteFile(filename, newFilename, keepExtension = true) {
857
+ const preview = previewRenameNoteFile(filename, newFilename, keepExtension);
858
+ const nextFilename = fileWriter.renameLocalNoteFile(filename, newFilename, keepExtension);
859
+ const renamedNote = fileReader.readNoteFile(nextFilename);
860
+ if (!renamedNote) {
861
+ throw new Error('Failed to read note after rename');
862
+ }
863
+ invalidateListingCaches();
864
+ return {
865
+ note: renamedNote,
866
+ fromFilename: preview.fromFilename,
867
+ toFilename: preview.toFilename,
868
+ };
869
+ }
870
+ export function renameSpaceNote(identifier, newTitle) {
871
+ const note = getNoteByIdentifierOrThrow(identifier);
872
+ if (note.source !== 'space') {
873
+ throw new Error('renameSpaceNote is for TeamSpace notes only');
874
+ }
875
+ if (note.type !== 'note') {
876
+ throw new Error('Renaming is supported for project notes only');
877
+ }
878
+ const fromTitle = note.title;
879
+ const writeId = note.id || note.filename;
880
+ sqliteWriter.updateSpaceNoteTitle(writeId, newTitle);
881
+ const renamedNote = sqliteReader.getSpaceNote(writeId);
882
+ if (!renamedNote) {
883
+ throw new Error('Failed to read note after rename');
884
+ }
885
+ invalidateListingCaches();
886
+ return {
887
+ note: renamedNote,
888
+ fromTitle,
889
+ toTitle: newTitle,
890
+ };
891
+ }
892
+ /**
893
+ * Get today's daily note
894
+ */
895
+ export function getTodayNote(space) {
896
+ const dateStr = getTodayDateString();
897
+ return getCalendarNote(dateStr, space);
898
+ }
899
+ /**
900
+ * Get a calendar note by date
901
+ */
902
+ export function getCalendarNote(date, space) {
903
+ const dateStr = parseFlexibleDate(date);
904
+ const resolvedSpace = resolveSpaceId(space);
905
+ if (resolvedSpace) {
906
+ return sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
907
+ }
908
+ return fileReader.getCalendarNote(dateStr);
909
+ }
910
+ /**
911
+ * Ensure a calendar note exists, create if not
912
+ */
913
+ export function ensureCalendarNote(date, space) {
914
+ const dateStr = parseFlexibleDate(date);
915
+ const resolvedSpace = resolveSpaceId(space);
916
+ if (resolvedSpace) {
917
+ let note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
918
+ if (!note) {
919
+ sqliteWriter.createSpaceCalendarNote(resolvedSpace, dateStr, '');
920
+ note = sqliteReader.getSpaceCalendarNote(dateStr, resolvedSpace);
921
+ invalidateListingCaches();
922
+ }
923
+ if (!note)
924
+ throw new Error('Failed to create space calendar note');
925
+ return note;
926
+ }
927
+ const filename = fileWriter.ensureCalendarNote(dateStr);
928
+ const note = fileReader.readNoteFile(filename);
929
+ if (!note)
930
+ throw new Error('Failed to create calendar note');
931
+ invalidateListingCaches();
932
+ return note;
933
+ }
934
+ /**
935
+ * Add content to today's note
936
+ */
937
+ export function addToToday(content, position = 'end', space) {
938
+ const note = ensureCalendarNote('today', space);
939
+ let newContent;
940
+ if (position === 'start') {
941
+ const lines = note.content.split('\n');
942
+ // Find end of frontmatter if present
943
+ let insertIndex = 0;
944
+ if (lines[0]?.trim() === '---') {
945
+ for (let i = 1; i < lines.length; i++) {
946
+ if (lines[i]?.trim() === '---') {
947
+ insertIndex = i + 1;
948
+ break;
949
+ }
950
+ }
951
+ }
952
+ lines.splice(insertIndex, 0, content);
953
+ newContent = lines.join('\n');
954
+ }
955
+ else {
956
+ newContent = note.content + (note.content.endsWith('\n') ? '' : '\n') + content;
957
+ }
958
+ const identifier = note.source === 'space' ? (note.id || note.filename) : note.filename;
959
+ return updateNote(identifier, newContent, { source: note.source });
960
+ }
961
+ /**
962
+ * List all spaces
963
+ */
964
+ export function listSpaces() {
965
+ return sqliteReader.listSpaces();
966
+ }
967
+ // Keep old name for backwards compatibility
968
+ export const listTeamspaces = listSpaces;
969
+ /**
970
+ * List folders with optional source/depth/query filtering
971
+ */
972
+ export function listFolders(options = {}) {
973
+ const resolvedSpace = resolveSpaceId(options.space);
974
+ const { includeLocal = !resolvedSpace, includeSpaces = Boolean(resolvedSpace), query, maxDepth, parentPath, recursive = true, } = options;
975
+ const space = resolvedSpace;
976
+ const normalizedQuery = query?.trim().toLowerCase() || '';
977
+ const normalizedParentPath = normalizeLocalFolderFilter(parentPath);
978
+ const cacheKey = JSON.stringify({
979
+ space: space || '',
980
+ includeLocal,
981
+ includeSpaces,
982
+ query: normalizedQuery,
983
+ maxDepth: typeof maxDepth === 'number' ? maxDepth : null,
984
+ parentPath: normalizedParentPath || '',
985
+ recursive,
986
+ });
987
+ const cached = getCachedValue(listFoldersCache, cacheKey);
988
+ if (cached) {
989
+ return cached;
990
+ }
991
+ const folders = [];
992
+ if (includeLocal) {
993
+ folders.push(...fileReader.listFolders(maxDepth));
994
+ }
995
+ if (includeSpaces) {
996
+ folders.push(...sqliteReader.listSpaceFolders(space));
997
+ }
998
+ const deduped = folders.filter((folder, index, arr) => {
999
+ const key = `${folder.source}:${folder.spaceId || ''}:${folder.path}`;
1000
+ return arr.findIndex((candidate) => `${candidate.source}:${candidate.spaceId || ''}:${candidate.path}` === key) === index;
1001
+ });
1002
+ let filtered = normalizedQuery
1003
+ ? deduped.filter((folder) => {
1004
+ const path = folder.path.toLowerCase();
1005
+ const name = folder.name.toLowerCase();
1006
+ return path.includes(normalizedQuery) || name.includes(normalizedQuery);
1007
+ })
1008
+ : deduped;
1009
+ if (normalizedParentPath) {
1010
+ const parentPrefix = `${normalizedParentPath}/`;
1011
+ filtered = filtered.filter((folder) => {
1012
+ const normalizedPath = folder.path.replace(/\\/g, '/');
1013
+ if (!normalizedPath.startsWith(parentPrefix))
1014
+ return false;
1015
+ if (recursive)
1016
+ return true;
1017
+ const relative = normalizedPath.slice(parentPrefix.length);
1018
+ return relative.length > 0 && !relative.includes('/');
1019
+ });
1020
+ }
1021
+ else if (!recursive) {
1022
+ filtered = filtered.filter((folder) => !folder.path.includes('/'));
1023
+ }
1024
+ filtered.sort((a, b) => a.path.localeCompare(b.path));
1025
+ return setCachedValue(listFoldersCache, cacheKey, filtered, LIST_FOLDERS_CACHE_TTL_MS);
1026
+ }
1027
+ function resolveSpaceFolderReference(spaceId, reference, options = {}) {
1028
+ const normalized = reference.trim();
1029
+ if (!normalized) {
1030
+ throw new Error('Folder reference is required');
1031
+ }
1032
+ const lower = normalized.toLowerCase();
1033
+ if (options.allowRoot === true && (lower === 'root' || lower === 'space-root' || normalized === spaceId)) {
1034
+ const space = sqliteReader.listSpaces().find((candidate) => candidate.id === spaceId);
1035
+ return {
1036
+ id: spaceId,
1037
+ path: space?.name || spaceId,
1038
+ name: space?.name || spaceId,
1039
+ };
1040
+ }
1041
+ const folder = sqliteReader.resolveSpaceFolder(spaceId, normalized, {
1042
+ includeTrash: options.includeTrash === true,
1043
+ });
1044
+ if (!folder?.id) {
1045
+ throw new Error(`Folder not found in space: ${reference}`);
1046
+ }
1047
+ return {
1048
+ id: folder.id,
1049
+ path: folder.path,
1050
+ name: folder.name,
1051
+ };
1052
+ }
1053
+ export function previewCreateFolder(options) {
1054
+ if ('space' in options) {
1055
+ const spaceId = resolveSpaceId(options.space.trim());
1056
+ if (!spaceId) {
1057
+ throw new Error('space is required');
1058
+ }
1059
+ const name = options.name.trim();
1060
+ if (!name) {
1061
+ throw new Error('name is required');
1062
+ }
1063
+ const parent = options.parent?.trim();
1064
+ const destination = parent
1065
+ ? resolveSpaceFolderReference(spaceId, parent, { allowRoot: true, includeTrash: true })
1066
+ : resolveSpaceFolderReference(spaceId, spaceId, { allowRoot: true, includeTrash: true });
1067
+ if (destination.name.toLowerCase() === '@trash') {
1068
+ throw new Error('Destination parent cannot be @Trash');
1069
+ }
1070
+ return {
1071
+ source: 'space',
1072
+ id: '(pending)',
1073
+ path: destination.id === spaceId ? name : `${destination.path}/${name}`,
1074
+ name,
1075
+ spaceId,
1076
+ parentId: destination.id,
1077
+ };
1078
+ }
1079
+ const folder = fileWriter.previewCreateFolder(options.path);
1080
+ return {
1081
+ source: 'local',
1082
+ path: folder,
1083
+ name: path.basename(folder),
1084
+ };
1085
+ }
1086
+ export function createFolder(options) {
1087
+ const preview = previewCreateFolder(options);
1088
+ if ('space' in options) {
1089
+ if (preview.source !== 'space') {
1090
+ throw new Error('Invalid folder create state');
1091
+ }
1092
+ const createdId = sqliteWriter.createSpaceFolder(preview.spaceId, preview.name, preview.parentId);
1093
+ const createdFolder = resolveSpaceFolderReference(preview.spaceId, createdId, {
1094
+ allowRoot: false,
1095
+ includeTrash: true,
1096
+ });
1097
+ invalidateListingCaches();
1098
+ return {
1099
+ source: 'space',
1100
+ id: createdFolder.id,
1101
+ path: createdFolder.path,
1102
+ name: createdFolder.name,
1103
+ spaceId: preview.spaceId,
1104
+ parentId: preview.parentId,
1105
+ };
1106
+ }
1107
+ const createdPath = fileWriter.createFolder(options.path);
1108
+ invalidateListingCaches();
1109
+ return {
1110
+ source: 'local',
1111
+ path: createdPath,
1112
+ name: path.basename(createdPath),
1113
+ };
1114
+ }
1115
+ export function previewMoveFolder(options) {
1116
+ if ('space' in options) {
1117
+ const spaceId = resolveSpaceId(options.space.trim());
1118
+ if (!spaceId) {
1119
+ throw new Error('space is required');
1120
+ }
1121
+ const source = resolveSpaceFolderReference(spaceId, options.source, {
1122
+ allowRoot: false,
1123
+ includeTrash: true,
1124
+ });
1125
+ const destination = resolveSpaceFolderReference(spaceId, options.destination, {
1126
+ allowRoot: true,
1127
+ includeTrash: true,
1128
+ });
1129
+ if (destination.name.toLowerCase() === '@trash') {
1130
+ throw new Error('Destination folder cannot be @Trash');
1131
+ }
1132
+ const counts = sqliteReader.countSpaceFolderContents(source.id);
1133
+ return {
1134
+ source: 'space',
1135
+ spaceId,
1136
+ folderId: source.id,
1137
+ fromPath: source.path,
1138
+ toPath: destination.id === spaceId ? source.name : `${destination.path}/${source.name}`,
1139
+ destinationParentId: destination.id,
1140
+ affectedNoteCount: counts.noteCount,
1141
+ affectedFolderCount: counts.folderCount,
1142
+ };
1143
+ }
1144
+ const preview = fileWriter.previewMoveLocalFolder(options.sourcePath, options.destinationFolder);
1145
+ const fullPath = path.join(fileReader.getNotesPath(), preview.fromFolder);
1146
+ const counts = fileReader.countNotesInDirectory(fullPath);
1147
+ return {
1148
+ source: 'local',
1149
+ fromPath: preview.fromFolder,
1150
+ toPath: preview.toFolder,
1151
+ destinationFolder: preview.destinationFolder || options.destinationFolder,
1152
+ affectedNoteCount: counts.noteCount,
1153
+ affectedFolderCount: counts.folderCount,
1154
+ };
1155
+ }
1156
+ export function moveFolder(options) {
1157
+ const preview = previewMoveFolder(options);
1158
+ if ('space' in options) {
1159
+ if (preview.source !== 'space') {
1160
+ throw new Error('Invalid folder move state');
1161
+ }
1162
+ sqliteWriter.moveSpaceFolder(preview.folderId, preview.destinationParentId);
1163
+ const moved = resolveSpaceFolderReference(preview.spaceId, preview.folderId, {
1164
+ allowRoot: false,
1165
+ includeTrash: true,
1166
+ });
1167
+ invalidateListingCaches();
1168
+ return {
1169
+ ...preview,
1170
+ toPath: moved.path,
1171
+ };
1172
+ }
1173
+ const moved = fileWriter.moveLocalFolder(options.sourcePath, options.destinationFolder);
1174
+ invalidateListingCaches();
1175
+ return {
1176
+ source: 'local',
1177
+ fromPath: moved.fromFolder,
1178
+ toPath: moved.toFolder,
1179
+ destinationFolder: moved.destinationFolder || options.destinationFolder,
1180
+ };
1181
+ }
1182
+ export function previewDeleteFolder(options) {
1183
+ if ('space' in options) {
1184
+ const spaceId = resolveSpaceId(options.space.trim());
1185
+ if (!spaceId) {
1186
+ throw new Error('space is required');
1187
+ }
1188
+ const source = resolveSpaceFolderReference(spaceId, options.source, {
1189
+ allowRoot: false,
1190
+ includeTrash: true,
1191
+ });
1192
+ const counts = sqliteReader.countSpaceFolderContents(source.id);
1193
+ return {
1194
+ source: 'space',
1195
+ spaceId,
1196
+ folderId: source.id,
1197
+ fromPath: source.path,
1198
+ trashFolderId: '(pending)',
1199
+ affectedNoteCount: counts.noteCount,
1200
+ affectedFolderCount: counts.folderCount,
1201
+ };
1202
+ }
1203
+ const normalized = fileWriter.previewDeleteLocalFolder(options.path);
1204
+ const fullPath = path.join(fileReader.getNotesPath(), normalized);
1205
+ const counts = fileReader.countNotesInDirectory(fullPath);
1206
+ return {
1207
+ source: 'local',
1208
+ fromPath: normalized,
1209
+ trashedPath: '(pending)',
1210
+ affectedNoteCount: counts.noteCount,
1211
+ affectedFolderCount: counts.folderCount,
1212
+ };
1213
+ }
1214
+ export function deleteFolder(options) {
1215
+ const preview = previewDeleteFolder(options);
1216
+ if ('space' in options) {
1217
+ if (preview.source !== 'space') {
1218
+ throw new Error('Invalid folder delete state');
1219
+ }
1220
+ const deleted = sqliteWriter.deleteSpaceFolder(preview.folderId);
1221
+ invalidateListingCaches();
1222
+ return {
1223
+ source: 'space',
1224
+ spaceId: preview.spaceId,
1225
+ folderId: preview.folderId,
1226
+ fromPath: preview.fromPath,
1227
+ trashFolderId: deleted.trashFolderId,
1228
+ };
1229
+ }
1230
+ const trashedPath = fileWriter.deleteLocalFolder(options.path);
1231
+ invalidateListingCaches();
1232
+ return {
1233
+ source: 'local',
1234
+ fromPath: preview.fromPath,
1235
+ trashedPath,
1236
+ };
1237
+ }
1238
+ export function previewRenameFolder(options) {
1239
+ if ('space' in options) {
1240
+ const spaceId = resolveSpaceId(options.space.trim());
1241
+ if (!spaceId) {
1242
+ throw new Error('space is required');
1243
+ }
1244
+ const source = resolveSpaceFolderReference(spaceId, options.source, {
1245
+ allowRoot: false,
1246
+ includeTrash: true,
1247
+ });
1248
+ const newName = options.newName.trim();
1249
+ if (!newName) {
1250
+ throw new Error('newName is required');
1251
+ }
1252
+ const separatorIndex = source.path.lastIndexOf('/');
1253
+ const parentPath = separatorIndex >= 0 ? source.path.slice(0, separatorIndex) : '';
1254
+ return {
1255
+ source: 'space',
1256
+ spaceId,
1257
+ folderId: source.id,
1258
+ fromPath: source.path,
1259
+ toPath: parentPath ? `${parentPath}/${newName}` : newName,
1260
+ previousName: source.name,
1261
+ name: newName,
1262
+ };
1263
+ }
1264
+ const preview = fileWriter.previewRenameLocalFolder(options.sourcePath, options.newName);
1265
+ return {
1266
+ source: 'local',
1267
+ fromPath: preview.fromFolder,
1268
+ toPath: preview.toFolder,
1269
+ };
1270
+ }
1271
+ export function renameFolder(options) {
1272
+ const preview = previewRenameFolder(options);
1273
+ if ('space' in options) {
1274
+ if (preview.source !== 'space') {
1275
+ throw new Error('Invalid folder rename state');
1276
+ }
1277
+ const renamed = sqliteWriter.renameSpaceFolder(preview.folderId, preview.name);
1278
+ const folder = resolveSpaceFolderReference(preview.spaceId, renamed.folderId, {
1279
+ allowRoot: false,
1280
+ includeTrash: true,
1281
+ });
1282
+ invalidateListingCaches();
1283
+ return {
1284
+ ...preview,
1285
+ toPath: folder.path,
1286
+ };
1287
+ }
1288
+ const renamed = fileWriter.renameLocalFolder(options.sourcePath, options.newName);
1289
+ invalidateListingCaches();
1290
+ return {
1291
+ source: 'local',
1292
+ fromPath: renamed.fromFolder,
1293
+ toPath: renamed.toFolder,
1294
+ };
1295
+ }
1296
+ /**
1297
+ * List all tags
1298
+ */
1299
+ export function listTags(space) {
1300
+ const resolvedSpace = resolveSpaceId(space);
1301
+ const tags = new Set();
1302
+ if (!resolvedSpace) {
1303
+ fileReader.extractAllTags().forEach((tag) => tags.add(tag));
1304
+ }
1305
+ sqliteReader.extractSpaceTags(resolvedSpace).forEach((tag) => tags.add(tag));
1306
+ return Array.from(tags).sort();
1307
+ }
1308
+ //# sourceMappingURL=unified-store.js.map