@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.
- package/README.md +257 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/noteplan/embeddings.d.ts +170 -0
- package/dist/noteplan/embeddings.d.ts.map +1 -0
- package/dist/noteplan/embeddings.js +684 -0
- package/dist/noteplan/embeddings.js.map +1 -0
- package/dist/noteplan/file-reader.d.ts +77 -0
- package/dist/noteplan/file-reader.d.ts.map +1 -0
- package/dist/noteplan/file-reader.js +488 -0
- package/dist/noteplan/file-reader.js.map +1 -0
- package/dist/noteplan/file-writer.d.ts +108 -0
- package/dist/noteplan/file-writer.d.ts.map +1 -0
- package/dist/noteplan/file-writer.js +621 -0
- package/dist/noteplan/file-writer.js.map +1 -0
- package/dist/noteplan/filter-store.d.ts +28 -0
- package/dist/noteplan/filter-store.d.ts.map +1 -0
- package/dist/noteplan/filter-store.js +180 -0
- package/dist/noteplan/filter-store.js.map +1 -0
- package/dist/noteplan/frontmatter-parser.d.ts +45 -0
- package/dist/noteplan/frontmatter-parser.d.ts.map +1 -0
- package/dist/noteplan/frontmatter-parser.js +259 -0
- package/dist/noteplan/frontmatter-parser.js.map +1 -0
- package/dist/noteplan/fuzzy-search.d.ts +7 -0
- package/dist/noteplan/fuzzy-search.d.ts.map +1 -0
- package/dist/noteplan/fuzzy-search.js +66 -0
- package/dist/noteplan/fuzzy-search.js.map +1 -0
- package/dist/noteplan/markdown-parser.d.ts +87 -0
- package/dist/noteplan/markdown-parser.d.ts.map +1 -0
- package/dist/noteplan/markdown-parser.js +519 -0
- package/dist/noteplan/markdown-parser.js.map +1 -0
- package/dist/noteplan/preferences.d.ts +44 -0
- package/dist/noteplan/preferences.d.ts.map +1 -0
- package/dist/noteplan/preferences.js +156 -0
- package/dist/noteplan/preferences.js.map +1 -0
- package/dist/noteplan/ripgrep-search.d.ts +29 -0
- package/dist/noteplan/ripgrep-search.d.ts.map +1 -0
- package/dist/noteplan/ripgrep-search.js +110 -0
- package/dist/noteplan/ripgrep-search.js.map +1 -0
- package/dist/noteplan/sqlite-reader.d.ts +77 -0
- package/dist/noteplan/sqlite-reader.d.ts.map +1 -0
- package/dist/noteplan/sqlite-reader.js +605 -0
- package/dist/noteplan/sqlite-reader.js.map +1 -0
- package/dist/noteplan/sqlite-writer.d.ts +63 -0
- package/dist/noteplan/sqlite-writer.d.ts.map +1 -0
- package/dist/noteplan/sqlite-writer.js +574 -0
- package/dist/noteplan/sqlite-writer.js.map +1 -0
- package/dist/noteplan/types.d.ts +97 -0
- package/dist/noteplan/types.d.ts.map +1 -0
- package/dist/noteplan/types.js +33 -0
- package/dist/noteplan/types.js.map +1 -0
- package/dist/noteplan/unified-store.d.ts +289 -0
- package/dist/noteplan/unified-store.d.ts.map +1 -0
- package/dist/noteplan/unified-store.js +1308 -0
- package/dist/noteplan/unified-store.js.map +1 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +2468 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/calendar.d.ts +311 -0
- package/dist/tools/calendar.d.ts.map +1 -0
- package/dist/tools/calendar.js +504 -0
- package/dist/tools/calendar.js.map +1 -0
- package/dist/tools/embeddings.d.ts +244 -0
- package/dist/tools/embeddings.d.ts.map +1 -0
- package/dist/tools/embeddings.js +226 -0
- package/dist/tools/embeddings.js.map +1 -0
- package/dist/tools/events.d.ts +176 -0
- package/dist/tools/events.d.ts.map +1 -0
- package/dist/tools/events.js +326 -0
- package/dist/tools/events.js.map +1 -0
- package/dist/tools/filters.d.ts +205 -0
- package/dist/tools/filters.d.ts.map +1 -0
- package/dist/tools/filters.js +347 -0
- package/dist/tools/filters.js.map +1 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +161 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/notes.d.ts +1221 -0
- package/dist/tools/notes.d.ts.map +1 -0
- package/dist/tools/notes.js +1868 -0
- package/dist/tools/notes.js.map +1 -0
- package/dist/tools/plugins.d.ts +140 -0
- package/dist/tools/plugins.d.ts.map +1 -0
- package/dist/tools/plugins.js +782 -0
- package/dist/tools/plugins.js.map +1 -0
- package/dist/tools/reminders.d.ts +207 -0
- package/dist/tools/reminders.d.ts.map +1 -0
- package/dist/tools/reminders.js +323 -0
- package/dist/tools/reminders.js.map +1 -0
- package/dist/tools/search.d.ts +58 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +373 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/spaces.d.ts +484 -0
- package/dist/tools/spaces.d.ts.map +1 -0
- package/dist/tools/spaces.js +870 -0
- package/dist/tools/spaces.js.map +1 -0
- package/dist/tools/tasks.d.ts +313 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +690 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/themes.d.ts +91 -0
- package/dist/tools/themes.d.ts.map +1 -0
- package/dist/tools/themes.js +294 -0
- package/dist/tools/themes.js.map +1 -0
- package/dist/tools/ui.d.ts +89 -0
- package/dist/tools/ui.d.ts.map +1 -0
- package/dist/tools/ui.js +137 -0
- package/dist/tools/ui.js.map +1 -0
- package/dist/utils/applescript.d.ts +5 -0
- package/dist/utils/applescript.d.ts.map +1 -0
- package/dist/utils/applescript.js +27 -0
- package/dist/utils/applescript.js.map +1 -0
- package/dist/utils/confirmation-tokens.d.ts +19 -0
- package/dist/utils/confirmation-tokens.d.ts.map +1 -0
- package/dist/utils/confirmation-tokens.js +58 -0
- package/dist/utils/confirmation-tokens.js.map +1 -0
- package/dist/utils/date-filters.d.ts +15 -0
- package/dist/utils/date-filters.d.ts.map +1 -0
- package/dist/utils/date-filters.js +129 -0
- package/dist/utils/date-filters.js.map +1 -0
- package/dist/utils/date-utils.d.ts +113 -0
- package/dist/utils/date-utils.d.ts.map +1 -0
- package/dist/utils/date-utils.js +341 -0
- package/dist/utils/date-utils.js.map +1 -0
- package/dist/utils/folder-matcher.d.ts +14 -0
- package/dist/utils/folder-matcher.d.ts.map +1 -0
- package/dist/utils/folder-matcher.js +191 -0
- package/dist/utils/folder-matcher.js.map +1 -0
- package/dist/utils/version.d.ts +10 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +88 -0
- package/dist/utils/version.js.map +1 -0
- package/docs/plugin-api/Calendar.md +448 -0
- package/docs/plugin-api/CalendarItem.md +198 -0
- package/docs/plugin-api/Clipboard.md +101 -0
- package/docs/plugin-api/CommandBar.md +251 -0
- package/docs/plugin-api/DataStore.md +700 -0
- package/docs/plugin-api/Editor.md +982 -0
- package/docs/plugin-api/HTMLView.md +337 -0
- package/docs/plugin-api/NoteObject.md +588 -0
- package/docs/plugin-api/NotePlan.md +398 -0
- package/docs/plugin-api/ParagraphObject.md +242 -0
- package/docs/plugin-api/RangeObject.md +56 -0
- package/docs/plugin-api/getting-started.md +545 -0
- package/docs/plugin-api/plugin-api-condensed.md +526 -0
- package/docs/plugin-api/plugin.json +26 -0
- package/docs/plugin-api/script.js +542 -0
- package/package.json +60 -0
- package/scripts/calendar-helper +0 -0
- 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
|