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