@noteplanco/noteplan-mcp 1.1.23 → 1.1.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +315 -221
- 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 +257 -159
- package/dist/noteplan/unified-store.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +142 -61
- 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 +16 -13
- package/dist/tools/calendar.d.ts.map +1 -1
- package/dist/tools/calendar.js +17 -16
- 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 +180 -177
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/notes.test.js +242 -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.d.ts.map +1 -1
- package/dist/tools/themes.js +26 -35
- 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
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
// File system reader for local NotePlan notes
|
|
2
|
+
//
|
|
3
|
+
// All exported I/O functions are async and route through the MCP bridge
|
|
4
|
+
// when NotePlan is running (avoiding TCC prompts), falling back to direct
|
|
5
|
+
// fs access otherwise. The path detection at the bottom of the file stays
|
|
6
|
+
// synchronous because it bootstraps once at startup before any caller can
|
|
7
|
+
// await anything.
|
|
2
8
|
import * as fs from 'fs';
|
|
3
9
|
import * as path from 'path';
|
|
4
10
|
import * as os from 'os';
|
|
@@ -7,6 +13,9 @@ import { extractTitle, extractTagsFromContent } from './markdown-parser.js';
|
|
|
7
13
|
import { extractDateFromFilename } from '../utils/date-utils.js';
|
|
8
14
|
import { getDetectedAppName } from '../utils/version.js';
|
|
9
15
|
import { normalizeFilename } from '../utils/filename-normalize.js';
|
|
16
|
+
import { getBridgeClient } from '../transport/bridge-availability.js';
|
|
17
|
+
import { readFileUtf8, statPath, readDir } from '../transport/bridge-fs.js';
|
|
18
|
+
import { isRipgrepAvailable, ripgrepOnlyMatching } from './ripgrep-search.js';
|
|
10
19
|
/** Valid note file extensions in NotePlan */
|
|
11
20
|
const VALID_NOTE_EXTENSIONS = ['.md', '.txt'];
|
|
12
21
|
/**
|
|
@@ -18,6 +27,25 @@ export function isValidNoteExtension(filename) {
|
|
|
18
27
|
const ext = path.extname(filename).toLowerCase();
|
|
19
28
|
return VALID_NOTE_EXTENSIONS.includes(ext);
|
|
20
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Folders that should be skipped when listing/recursing the user's notes:
|
|
32
|
+
* - dot-prefixed (e.g. .DS_Store, .git)
|
|
33
|
+
* - NotePlan's @Trash and @Archive system folders
|
|
34
|
+
* - <NoteName>_attachments folders that NotePlan auto-creates next to
|
|
35
|
+
* notes that have images/files attached. They're not user-organized
|
|
36
|
+
* folders and surfacing them confuses tools that just want the
|
|
37
|
+
* organizational tree.
|
|
38
|
+
*/
|
|
39
|
+
function isHiddenFolder(name) {
|
|
40
|
+
if (name.startsWith('.'))
|
|
41
|
+
return true;
|
|
42
|
+
if (name === '@Trash' || name === '@Archive')
|
|
43
|
+
return true;
|
|
44
|
+
if (name.endsWith('_attachments'))
|
|
45
|
+
return true;
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
// MARK: - Storage path detection (sync, runs once at startup)
|
|
21
49
|
// Possible NotePlan storage paths (in order of preference)
|
|
22
50
|
const POSSIBLE_PATHS = [
|
|
23
51
|
// Direct local paths (AppStore version) - preferred for local dev
|
|
@@ -34,9 +62,6 @@ const POSSIBLE_PATHS = [
|
|
|
34
62
|
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~co~noteplan~NotePlan-setapp/Documents'),
|
|
35
63
|
];
|
|
36
64
|
let cachedConfig = null;
|
|
37
|
-
/**
|
|
38
|
-
* Detect the file extension used in a directory by examining existing files
|
|
39
|
-
*/
|
|
40
65
|
function detectFileExtension(calendarPath) {
|
|
41
66
|
try {
|
|
42
67
|
const entries = fs.readdirSync(calendarPath, { withFileTypes: true });
|
|
@@ -46,7 +71,6 @@ function detectFileExtension(calendarPath) {
|
|
|
46
71
|
let newestMd = 0;
|
|
47
72
|
for (const entry of entries) {
|
|
48
73
|
if (entry.isFile()) {
|
|
49
|
-
// Check for daily note pattern (YYYYMMDD)
|
|
50
74
|
if (/^\d{8}\.(txt|md)$/.test(entry.name)) {
|
|
51
75
|
const filePath = path.join(calendarPath, entry.name);
|
|
52
76
|
const stats = fs.statSync(filePath);
|
|
@@ -61,30 +85,23 @@ function detectFileExtension(calendarPath) {
|
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
87
|
}
|
|
64
|
-
// Prefer the extension with the most recent file, then by count
|
|
65
88
|
if (newestTxt > newestMd)
|
|
66
89
|
return '.txt';
|
|
67
90
|
if (newestMd > newestTxt)
|
|
68
91
|
return '.md';
|
|
69
92
|
if (txtCount > mdCount)
|
|
70
93
|
return '.txt';
|
|
71
|
-
return '.md';
|
|
94
|
+
return '.md';
|
|
72
95
|
}
|
|
73
96
|
catch {
|
|
74
|
-
return '.txt';
|
|
97
|
+
return '.txt';
|
|
75
98
|
}
|
|
76
99
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Check if calendar notes use year subfolders (Calendar/2024/20240101.txt)
|
|
79
|
-
* or flat structure (Calendar/20240101.txt)
|
|
80
|
-
*/
|
|
81
100
|
function detectYearSubfolders(calendarPath) {
|
|
82
101
|
try {
|
|
83
102
|
const entries = fs.readdirSync(calendarPath, { withFileTypes: true });
|
|
84
|
-
// Check for year directories (4 digits)
|
|
85
103
|
for (const entry of entries) {
|
|
86
104
|
if (entry.isDirectory() && /^\d{4}$/.test(entry.name)) {
|
|
87
|
-
// Verify it contains calendar notes
|
|
88
105
|
const yearPath = path.join(calendarPath, entry.name);
|
|
89
106
|
const yearEntries = fs.readdirSync(yearPath);
|
|
90
107
|
if (yearEntries.some(f => /^\d{8}\.(txt|md)$/.test(f))) {
|
|
@@ -92,39 +109,29 @@ function detectYearSubfolders(calendarPath) {
|
|
|
92
109
|
}
|
|
93
110
|
}
|
|
94
111
|
}
|
|
95
|
-
// Check for flat structure (files directly in Calendar/)
|
|
96
112
|
for (const entry of entries) {
|
|
97
113
|
if (entry.isFile() && /^\d{8}\.(txt|md)$/.test(entry.name)) {
|
|
98
114
|
return false;
|
|
99
115
|
}
|
|
100
116
|
}
|
|
101
|
-
return false;
|
|
117
|
+
return false;
|
|
102
118
|
}
|
|
103
119
|
catch {
|
|
104
120
|
return false;
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
|
-
/**
|
|
108
|
-
* Score a storage path based on most recent modification time
|
|
109
|
-
* Checks the root folder and top-level folders (Calendar, Notes) - folder mtime updates when contents change
|
|
110
|
-
*/
|
|
111
123
|
function scoreStoragePath(storagePath) {
|
|
112
124
|
let newestMtime = 0;
|
|
113
125
|
try {
|
|
114
|
-
// Check the root storage folder
|
|
115
126
|
const rootStats = fs.statSync(storagePath);
|
|
116
127
|
newestMtime = Math.max(newestMtime, rootStats.mtimeMs);
|
|
117
|
-
// Check Calendar folder
|
|
118
128
|
const calendarPath = path.join(storagePath, 'Calendar');
|
|
119
129
|
if (fs.existsSync(calendarPath)) {
|
|
120
|
-
|
|
121
|
-
newestMtime = Math.max(newestMtime, calendarStats.mtimeMs);
|
|
130
|
+
newestMtime = Math.max(newestMtime, fs.statSync(calendarPath).mtimeMs);
|
|
122
131
|
}
|
|
123
|
-
// Check Notes folder
|
|
124
132
|
const notesPath = path.join(storagePath, 'Notes');
|
|
125
133
|
if (fs.existsSync(notesPath)) {
|
|
126
|
-
|
|
127
|
-
newestMtime = Math.max(newestMtime, notesStats.mtimeMs);
|
|
134
|
+
newestMtime = Math.max(newestMtime, fs.statSync(notesPath).mtimeMs);
|
|
128
135
|
}
|
|
129
136
|
}
|
|
130
137
|
catch {
|
|
@@ -132,32 +139,22 @@ function scoreStoragePath(storagePath) {
|
|
|
132
139
|
}
|
|
133
140
|
return newestMtime;
|
|
134
141
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Check if a path contains a valid NotePlan structure (has Calendar or Notes folder)
|
|
137
|
-
*/
|
|
138
142
|
function isValidNotePlanPath(storagePath) {
|
|
139
143
|
if (!fs.existsSync(storagePath))
|
|
140
144
|
return false;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return hasCalendar || hasNotes;
|
|
145
|
+
return fs.existsSync(path.join(storagePath, 'Calendar')) ||
|
|
146
|
+
fs.existsSync(path.join(storagePath, 'Notes'));
|
|
144
147
|
}
|
|
145
|
-
/**
|
|
146
|
-
* Ask the running NotePlan app for its storage path via AppleScript.
|
|
147
|
-
* Returns the path if successful, null otherwise.
|
|
148
|
-
*/
|
|
149
148
|
function detectStoragePathViaAppleScript() {
|
|
150
149
|
try {
|
|
151
150
|
const appName = getDetectedAppName();
|
|
152
|
-
// Check if NotePlan is running first — avoid launching the app via AppleScript
|
|
153
151
|
const isRunning = execFileSync('osascript', ['-e', `application "${appName}" is running`], {
|
|
154
152
|
encoding: 'utf-8',
|
|
155
153
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
156
154
|
timeout: 3_000,
|
|
157
155
|
}).trim();
|
|
158
|
-
if (isRunning !== 'true')
|
|
156
|
+
if (isRunning !== 'true')
|
|
159
157
|
return null;
|
|
160
|
-
}
|
|
161
158
|
const result = execFileSync('osascript', ['-e', `tell application "${appName}" to getStoragePath`], {
|
|
162
159
|
encoding: 'utf-8',
|
|
163
160
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -173,27 +170,16 @@ function detectStoragePathViaAppleScript() {
|
|
|
173
170
|
}
|
|
174
171
|
return null;
|
|
175
172
|
}
|
|
176
|
-
/**
|
|
177
|
-
* CloudKit (container) paths — used when isUsingCloudKit is true
|
|
178
|
-
*/
|
|
179
173
|
const CLOUDKIT_PATHS = [
|
|
180
174
|
path.join(os.homedir(), 'Library/Containers/co.noteplan.NotePlan3/Data/Library/Application Support/co.noteplan.NotePlan3'),
|
|
181
175
|
path.join(os.homedir(), 'Library/Containers/co.noteplan.NotePlan-setapp/Data/Library/Application Support/co.noteplan.NotePlan-setapp'),
|
|
182
176
|
];
|
|
183
|
-
/**
|
|
184
|
-
* iCloud Drive paths — used when isUsingCloudKit is false
|
|
185
|
-
*/
|
|
186
177
|
const ICLOUD_DRIVE_PATHS = [
|
|
187
178
|
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~co~noteplan~Today/Documents'),
|
|
188
179
|
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~co~noteplan~NotePlan3/Documents'),
|
|
189
180
|
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~co~noteplan~NotePlan/Documents'),
|
|
190
181
|
path.join(os.homedir(), 'Library/Mobile Documents/iCloud~co~noteplan~NotePlan-setapp/Documents'),
|
|
191
182
|
];
|
|
192
|
-
/**
|
|
193
|
-
* Read the sync method from NotePlan's UserDefaults and return the matching storage path.
|
|
194
|
-
* More reliable than filesystem scoring since it reads the actual user preference.
|
|
195
|
-
* Returns null if the preference can't be read or no valid path is found.
|
|
196
|
-
*/
|
|
197
183
|
function detectStoragePathViaUserDefaults() {
|
|
198
184
|
try {
|
|
199
185
|
const result = execFileSync('defaults', ['read', 'co.noteplan.NotePlan3', 'isUsingCloudKit'], {
|
|
@@ -215,19 +201,13 @@ function detectStoragePathViaUserDefaults() {
|
|
|
215
201
|
}
|
|
216
202
|
return null;
|
|
217
203
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Detect and cache NotePlan configuration
|
|
220
|
-
*/
|
|
221
204
|
function detectConfig() {
|
|
222
205
|
if (cachedConfig)
|
|
223
206
|
return cachedConfig;
|
|
224
|
-
// First, ask the running app directly
|
|
225
207
|
let bestPath = detectStoragePathViaAppleScript();
|
|
226
|
-
// Try UserDefaults to determine sync method before falling back to filesystem scoring
|
|
227
208
|
if (!bestPath) {
|
|
228
209
|
bestPath = detectStoragePathViaUserDefaults();
|
|
229
210
|
}
|
|
230
|
-
// Last resort: filesystem scoring (least reliable — can pick wrong folder)
|
|
231
211
|
if (!bestPath) {
|
|
232
212
|
let bestScore = -1;
|
|
233
213
|
for (const storagePath of POSSIBLE_PATHS) {
|
|
@@ -245,7 +225,6 @@ function detectConfig() {
|
|
|
245
225
|
}
|
|
246
226
|
const calendarPath = path.join(bestPath, 'Calendar');
|
|
247
227
|
const hasYearSubfolders = detectYearSubfolders(calendarPath);
|
|
248
|
-
// Detect extension in the right location
|
|
249
228
|
const extensionDetectPath = hasYearSubfolders
|
|
250
229
|
? path.join(calendarPath, new Date().getFullYear().toString())
|
|
251
230
|
: calendarPath;
|
|
@@ -260,45 +239,25 @@ function detectConfig() {
|
|
|
260
239
|
console.error(`NotePlan config: ${bestPath} (ext: ${fileExtension}, yearFolders: ${hasYearSubfolders})`);
|
|
261
240
|
return cachedConfig;
|
|
262
241
|
}
|
|
263
|
-
|
|
264
|
-
* Get the NotePlan storage path
|
|
265
|
-
*/
|
|
242
|
+
// MARK: - Synchronous path getters (read cached config)
|
|
266
243
|
export function getNotePlanPath() {
|
|
267
244
|
return detectConfig().storagePath;
|
|
268
245
|
}
|
|
269
|
-
/**
|
|
270
|
-
* Get the detected file extension
|
|
271
|
-
*/
|
|
272
246
|
export function getFileExtension() {
|
|
273
247
|
return detectConfig().fileExtension;
|
|
274
248
|
}
|
|
275
|
-
/**
|
|
276
|
-
* Get whether year subfolders are used
|
|
277
|
-
*/
|
|
278
249
|
export function hasYearSubfolders() {
|
|
279
250
|
return detectConfig().hasYearSubfolders;
|
|
280
251
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Get all available NotePlan storage paths (for multi-source searching)
|
|
283
|
-
*/
|
|
284
252
|
export function getAllNotePlanPaths() {
|
|
285
253
|
return POSSIBLE_PATHS.filter(isValidNotePlanPath);
|
|
286
254
|
}
|
|
287
|
-
/**
|
|
288
|
-
* Get the Calendar notes directory
|
|
289
|
-
*/
|
|
290
255
|
export function getCalendarPath() {
|
|
291
256
|
return path.join(getNotePlanPath(), 'Calendar');
|
|
292
257
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Get the project Notes directory
|
|
295
|
-
*/
|
|
296
258
|
export function getNotesPath() {
|
|
297
259
|
return path.join(getNotePlanPath(), 'Notes');
|
|
298
260
|
}
|
|
299
|
-
/**
|
|
300
|
-
* Build the calendar note file path for a given date
|
|
301
|
-
*/
|
|
302
261
|
export function buildCalendarNotePath(dateStr) {
|
|
303
262
|
const config = detectConfig();
|
|
304
263
|
const ext = config.fileExtension;
|
|
@@ -308,66 +267,102 @@ export function buildCalendarNotePath(dateStr) {
|
|
|
308
267
|
}
|
|
309
268
|
return `Calendar/${dateStr}${ext}`;
|
|
310
269
|
}
|
|
270
|
+
// NotePlan stores `defaultNoteExtension` as a real preference; the fs
|
|
271
|
+
// heuristic in detectFileExtension only counts existing files, so a user
|
|
272
|
+
// who recently switched to .md will still get .txt for new calendar notes
|
|
273
|
+
// until the new files outnumber the old. Calendar notes are extension-
|
|
274
|
+
// sensitive (NotePlan ignores wrong-extension files), so we ask the
|
|
275
|
+
// bridge for the truth whenever NotePlan is running.
|
|
276
|
+
const BRIDGE_EXT_TTL_MS = 60_000;
|
|
277
|
+
let bridgeFileExtensionCache = null;
|
|
278
|
+
/** @internal exposed for tests; the cache TTL prevents real-world leaks. */
|
|
279
|
+
export function __resetCalendarExtensionCache() {
|
|
280
|
+
bridgeFileExtensionCache = null;
|
|
281
|
+
}
|
|
282
|
+
export async function resolveNotePlanFileExtension() {
|
|
283
|
+
if (bridgeFileExtensionCache && bridgeFileExtensionCache.expiresAt > Date.now()) {
|
|
284
|
+
return bridgeFileExtensionCache.ext;
|
|
285
|
+
}
|
|
286
|
+
const bridge = await getBridgeClient();
|
|
287
|
+
if (bridge) {
|
|
288
|
+
try {
|
|
289
|
+
const config = await bridge.config();
|
|
290
|
+
if (config.fileExtension === '.md' || config.fileExtension === '.txt') {
|
|
291
|
+
bridgeFileExtensionCache = {
|
|
292
|
+
ext: config.fileExtension,
|
|
293
|
+
expiresAt: Date.now() + BRIDGE_EXT_TTL_MS,
|
|
294
|
+
};
|
|
295
|
+
return config.fileExtension;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Fall through to fs heuristic.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return detectConfig().fileExtension;
|
|
303
|
+
}
|
|
304
|
+
export async function buildCalendarNotePathAsync(dateStr) {
|
|
305
|
+
const ext = await resolveNotePlanFileExtension();
|
|
306
|
+
if (detectConfig().hasYearSubfolders) {
|
|
307
|
+
const year = dateStr.substring(0, 4);
|
|
308
|
+
return `Calendar/${year}/${dateStr}${ext}`;
|
|
309
|
+
}
|
|
310
|
+
return `Calendar/${dateStr}${ext}`;
|
|
311
|
+
}
|
|
312
|
+
// MARK: - Async I/O exports
|
|
311
313
|
/**
|
|
312
|
-
* Read a note file from the file system
|
|
314
|
+
* Read a note file from the file system (or via the bridge when NotePlan
|
|
315
|
+
* is running). Handles Unicode NFC/NFD path mismatches and rejects paths
|
|
316
|
+
* outside the storage root.
|
|
313
317
|
*/
|
|
314
|
-
export function readNoteFile(filePath) {
|
|
318
|
+
export async function readNoteFile(filePath) {
|
|
315
319
|
try {
|
|
316
320
|
const fullPath = path.isAbsolute(filePath) ? filePath : path.join(getNotePlanPath(), filePath);
|
|
317
|
-
// Reject paths outside the NotePlan data directory
|
|
318
321
|
const resolvedFull = path.resolve(fullPath);
|
|
319
322
|
const resolvedRoot = path.resolve(getNotePlanPath());
|
|
320
323
|
if (resolvedFull !== resolvedRoot && !resolvedFull.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
321
324
|
return null;
|
|
322
325
|
}
|
|
323
326
|
let resolvedPath = fullPath;
|
|
324
|
-
|
|
327
|
+
let stats = await statPath(resolvedPath);
|
|
328
|
+
if (!stats.exists) {
|
|
325
329
|
// Try Unicode-normalized form (NFC) — handles NFD/NFC mismatches on macOS
|
|
326
330
|
const nfcPath = normalizeFilename(resolvedPath);
|
|
327
|
-
if (nfcPath !== resolvedPath
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const dir = path.dirname(fullPath);
|
|
333
|
-
const targetBase = normalizeFilename(path.basename(fullPath));
|
|
334
|
-
if (fs.existsSync(dir)) {
|
|
335
|
-
try {
|
|
336
|
-
const entries = fs.readdirSync(dir);
|
|
337
|
-
const match = entries.find((e) => e.normalize('NFC') === targetBase);
|
|
338
|
-
if (match) {
|
|
339
|
-
resolvedPath = path.join(dir, match);
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
catch {
|
|
346
|
-
return null;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
return null;
|
|
331
|
+
if (nfcPath !== resolvedPath) {
|
|
332
|
+
const nfcStats = await statPath(nfcPath);
|
|
333
|
+
if (nfcStats.exists) {
|
|
334
|
+
resolvedPath = nfcPath;
|
|
335
|
+
stats = nfcStats;
|
|
351
336
|
}
|
|
352
337
|
}
|
|
353
338
|
}
|
|
354
|
-
|
|
339
|
+
if (!stats.exists) {
|
|
340
|
+
// Last resort: scan the parent directory for a Unicode-equivalent match
|
|
341
|
+
const dir = path.dirname(fullPath);
|
|
342
|
+
const targetBase = normalizeFilename(path.basename(fullPath));
|
|
343
|
+
const entries = await readDir(dir);
|
|
344
|
+
const match = entries.find((e) => e.name.normalize('NFC') === targetBase);
|
|
345
|
+
if (!match)
|
|
346
|
+
return null;
|
|
347
|
+
resolvedPath = path.join(dir, match.name);
|
|
348
|
+
stats = await statPath(resolvedPath);
|
|
349
|
+
if (!stats.exists)
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
if (stats.isDir)
|
|
353
|
+
return null;
|
|
355
354
|
const resolvedFinal = path.resolve(resolvedPath);
|
|
356
355
|
if (resolvedFinal !== resolvedRoot && !resolvedFinal.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
357
356
|
return null;
|
|
358
357
|
}
|
|
359
|
-
const stats = fs.statSync(resolvedPath);
|
|
360
|
-
if (stats.isDirectory()) {
|
|
361
|
-
return null;
|
|
362
|
-
}
|
|
363
|
-
// Only read files with valid note extensions (.md, .txt)
|
|
364
358
|
if (!isValidNoteExtension(path.basename(resolvedPath))) {
|
|
365
359
|
return null;
|
|
366
360
|
}
|
|
367
|
-
const content =
|
|
361
|
+
const content = await readFileUtf8(resolvedPath);
|
|
362
|
+
if (content === null)
|
|
363
|
+
return null;
|
|
368
364
|
const relativePath = path.relative(getNotePlanPath(), resolvedPath);
|
|
369
365
|
const filename = path.basename(resolvedPath);
|
|
370
|
-
// Determine note type
|
|
371
366
|
let type = 'note';
|
|
372
367
|
let date;
|
|
373
368
|
if (relativePath.startsWith('Calendar/') || relativePath.startsWith('Calendar\\')) {
|
|
@@ -378,7 +373,6 @@ export function readNoteFile(filePath) {
|
|
|
378
373
|
relativePath.startsWith('@Trash/') || relativePath.startsWith('@Trash\\')) {
|
|
379
374
|
type = 'trash';
|
|
380
375
|
}
|
|
381
|
-
// Extract folder from path
|
|
382
376
|
const folder = path.dirname(relativePath);
|
|
383
377
|
return {
|
|
384
378
|
id: relativePath,
|
|
@@ -390,7 +384,7 @@ export function readNoteFile(filePath) {
|
|
|
390
384
|
folder: folder !== '.' ? folder : undefined,
|
|
391
385
|
date,
|
|
392
386
|
modifiedAt: stats.mtime,
|
|
393
|
-
createdAt: stats.
|
|
387
|
+
createdAt: stats.ctime,
|
|
394
388
|
};
|
|
395
389
|
}
|
|
396
390
|
catch (error) {
|
|
@@ -399,37 +393,31 @@ export function readNoteFile(filePath) {
|
|
|
399
393
|
}
|
|
400
394
|
}
|
|
401
395
|
/**
|
|
402
|
-
* List all notes in a directory (recursive)
|
|
396
|
+
* List all notes in a directory (recursive).
|
|
403
397
|
*/
|
|
404
|
-
export function listNotesInDirectory(dirPath, type = 'note') {
|
|
398
|
+
export async function listNotesInDirectory(dirPath, type = 'note') {
|
|
405
399
|
const notes = [];
|
|
406
400
|
try {
|
|
407
401
|
const fullPath = path.isAbsolute(dirPath) ? dirPath : path.join(getNotePlanPath(), dirPath);
|
|
408
|
-
// Reject paths outside the NotePlan data directory
|
|
409
402
|
const resolvedFull = path.resolve(fullPath);
|
|
410
403
|
const resolvedRoot = path.resolve(getNotePlanPath());
|
|
411
404
|
if (resolvedFull !== resolvedRoot && !resolvedFull.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
412
405
|
return notes;
|
|
413
406
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
407
|
+
// readDir returns [] for missing dirs, so an explicit pathExists check
|
|
408
|
+
// would be a redundant stat round-trip per recursion level.
|
|
409
|
+
const entries = await readDir(fullPath);
|
|
418
410
|
for (const entry of entries) {
|
|
419
411
|
const entryPath = path.join(fullPath, entry.name);
|
|
420
|
-
if (entry.
|
|
421
|
-
|
|
422
|
-
if (entry.name.startsWith('.') || entry.name === '@Trash' || entry.name === '@Archive') {
|
|
412
|
+
if (entry.isDir) {
|
|
413
|
+
if (isHiddenFolder(entry.name))
|
|
423
414
|
continue;
|
|
424
|
-
|
|
425
|
-
// Recurse into subdirectories
|
|
426
|
-
notes.push(...listNotesInDirectory(entryPath, type));
|
|
415
|
+
notes.push(...(await listNotesInDirectory(entryPath, type)));
|
|
427
416
|
}
|
|
428
417
|
else if (entry.name.endsWith('.md') || entry.name.endsWith('.txt')) {
|
|
429
|
-
const note = readNoteFile(entryPath);
|
|
430
|
-
if (note)
|
|
418
|
+
const note = await readNoteFile(entryPath);
|
|
419
|
+
if (note)
|
|
431
420
|
notes.push(note);
|
|
432
|
-
}
|
|
433
421
|
}
|
|
434
422
|
}
|
|
435
423
|
}
|
|
@@ -439,22 +427,20 @@ export function listNotesInDirectory(dirPath, type = 'note') {
|
|
|
439
427
|
return notes;
|
|
440
428
|
}
|
|
441
429
|
/**
|
|
442
|
-
* Count notes and subfolders in a directory (recursive, lightweight — no file reads)
|
|
430
|
+
* Count notes and subfolders in a directory (recursive, lightweight — no file reads).
|
|
443
431
|
*/
|
|
444
|
-
export function countNotesInDirectory(dirPath) {
|
|
432
|
+
export async function countNotesInDirectory(dirPath) {
|
|
445
433
|
let noteCount = 0;
|
|
446
434
|
let folderCount = 0;
|
|
447
435
|
try {
|
|
448
436
|
const fullPath = path.isAbsolute(dirPath) ? dirPath : path.join(getNotePlanPath(), dirPath);
|
|
449
|
-
|
|
450
|
-
return { noteCount, folderCount };
|
|
451
|
-
const entries = fs.readdirSync(fullPath, { withFileTypes: true });
|
|
437
|
+
const entries = await readDir(fullPath);
|
|
452
438
|
for (const entry of entries) {
|
|
453
|
-
if (entry.
|
|
454
|
-
if (
|
|
439
|
+
if (entry.isDir) {
|
|
440
|
+
if (isHiddenFolder(entry.name))
|
|
455
441
|
continue;
|
|
456
442
|
folderCount++;
|
|
457
|
-
const sub = countNotesInDirectory(path.join(fullPath, entry.name));
|
|
443
|
+
const sub = await countNotesInDirectory(path.join(fullPath, entry.name));
|
|
458
444
|
noteCount += sub.noteCount;
|
|
459
445
|
folderCount += sub.folderCount;
|
|
460
446
|
}
|
|
@@ -468,117 +454,131 @@ export function countNotesInDirectory(dirPath) {
|
|
|
468
454
|
}
|
|
469
455
|
return { noteCount, folderCount };
|
|
470
456
|
}
|
|
471
|
-
|
|
472
|
-
* List all project notes
|
|
473
|
-
*/
|
|
474
|
-
export function listProjectNotes(folder) {
|
|
457
|
+
export async function listProjectNotes(folder) {
|
|
475
458
|
const basePath = folder ? path.join(getNotesPath(), folder) : getNotesPath();
|
|
476
459
|
return listNotesInDirectory(basePath, 'note');
|
|
477
460
|
}
|
|
478
|
-
|
|
479
|
-
* List all calendar notes
|
|
480
|
-
*/
|
|
481
|
-
export function listCalendarNotes(year) {
|
|
461
|
+
export async function listCalendarNotes(year) {
|
|
482
462
|
const basePath = year ? path.join(getCalendarPath(), year) : getCalendarPath();
|
|
483
463
|
return listNotesInDirectory(basePath, 'calendar');
|
|
484
464
|
}
|
|
485
465
|
/**
|
|
486
|
-
* Get a calendar note by date
|
|
466
|
+
* Get a calendar note by date — tries multiple file paths.
|
|
487
467
|
*/
|
|
488
|
-
export function getCalendarNote(dateStr) {
|
|
468
|
+
export async function getCalendarNote(dateStr) {
|
|
489
469
|
const config = detectConfig();
|
|
470
|
+
const preferredExt = await resolveNotePlanFileExtension();
|
|
490
471
|
const year = dateStr.substring(0, 4);
|
|
491
|
-
// Build list of paths to try (in order of preference)
|
|
492
472
|
const pathsToTry = [];
|
|
493
|
-
// First try the detected configuration
|
|
494
473
|
if (config.hasYearSubfolders) {
|
|
495
|
-
pathsToTry.push(`Calendar/${year}/${dateStr}${
|
|
496
|
-
|
|
497
|
-
const otherExt = config.fileExtension === '.txt' ? '.md' : '.txt';
|
|
474
|
+
pathsToTry.push(`Calendar/${year}/${dateStr}${preferredExt}`);
|
|
475
|
+
const otherExt = preferredExt === '.txt' ? '.md' : '.txt';
|
|
498
476
|
pathsToTry.push(`Calendar/${year}/${dateStr}${otherExt}`);
|
|
499
477
|
}
|
|
500
478
|
else {
|
|
501
|
-
pathsToTry.push(`Calendar/${dateStr}${
|
|
502
|
-
const otherExt =
|
|
479
|
+
pathsToTry.push(`Calendar/${dateStr}${preferredExt}`);
|
|
480
|
+
const otherExt = preferredExt === '.txt' ? '.md' : '.txt';
|
|
503
481
|
pathsToTry.push(`Calendar/${dateStr}${otherExt}`);
|
|
504
482
|
}
|
|
505
|
-
// Try flat structure with both extensions
|
|
506
483
|
pathsToTry.push(`Calendar/${dateStr}.txt`);
|
|
507
484
|
pathsToTry.push(`Calendar/${dateStr}.md`);
|
|
508
|
-
// Try year subfolder with both extensions
|
|
509
485
|
pathsToTry.push(`Calendar/${year}/${dateStr}.txt`);
|
|
510
486
|
pathsToTry.push(`Calendar/${year}/${dateStr}.md`);
|
|
511
|
-
// Deduplicate
|
|
512
487
|
const uniquePaths = [...new Set(pathsToTry)];
|
|
513
488
|
for (const filePath of uniquePaths) {
|
|
514
|
-
const note = readNoteFile(filePath);
|
|
489
|
+
const note = await readNoteFile(filePath);
|
|
515
490
|
if (note)
|
|
516
491
|
return note;
|
|
517
492
|
}
|
|
518
493
|
return null;
|
|
519
494
|
}
|
|
520
495
|
/**
|
|
521
|
-
* Get a note by title (searches project notes)
|
|
496
|
+
* Get a note by title (searches project notes).
|
|
522
497
|
*/
|
|
523
|
-
export function getNoteByTitle(title) {
|
|
524
|
-
const notes = listProjectNotes();
|
|
498
|
+
export async function getNoteByTitle(title) {
|
|
499
|
+
const notes = await listProjectNotes();
|
|
525
500
|
const lowerTitle = title.toLowerCase();
|
|
526
|
-
// Try exact match first
|
|
527
501
|
const exactMatch = notes.find((n) => n.title.toLowerCase() === lowerTitle);
|
|
528
502
|
if (exactMatch)
|
|
529
503
|
return exactMatch;
|
|
530
|
-
// Try filename match (without extension)
|
|
531
504
|
const filenameMatch = notes.find((n) => {
|
|
532
505
|
const basename = path.basename(n.filename, path.extname(n.filename));
|
|
533
506
|
return basename.toLowerCase() === lowerTitle;
|
|
534
507
|
});
|
|
535
508
|
if (filenameMatch)
|
|
536
509
|
return filenameMatch;
|
|
537
|
-
// Try partial match
|
|
538
510
|
const partialMatch = notes.find((n) => n.title.toLowerCase().includes(lowerTitle));
|
|
539
511
|
return partialMatch || null;
|
|
540
512
|
}
|
|
541
513
|
/**
|
|
542
|
-
* List all folders in the Notes directory
|
|
514
|
+
* List all folders in the Notes directory.
|
|
543
515
|
*/
|
|
544
|
-
export function listFolders(maxDepth) {
|
|
516
|
+
export async function listFolders(maxDepth) {
|
|
545
517
|
const folders = [];
|
|
546
|
-
function scanDir(dirPath, relativePath = '', depth = 0) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
// Recurse unless max depth reached
|
|
563
|
-
if (typeof maxDepth !== 'number' || nextDepth < maxDepth) {
|
|
564
|
-
scanDir(path.join(dirPath, entry.name), folderRelPath, nextDepth);
|
|
565
|
-
}
|
|
518
|
+
async function scanDir(dirPath, relativePath = '', depth = 0) {
|
|
519
|
+
const entries = await readDir(dirPath);
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
if (entry.isDir && !isHiddenFolder(entry.name)) {
|
|
522
|
+
const nextDepth = depth + 1;
|
|
523
|
+
if (typeof maxDepth === 'number' && nextDepth > maxDepth) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const folderRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
|
|
527
|
+
folders.push({
|
|
528
|
+
path: folderRelPath,
|
|
529
|
+
name: entry.name,
|
|
530
|
+
source: 'local',
|
|
531
|
+
});
|
|
532
|
+
if (typeof maxDepth !== 'number' || nextDepth < maxDepth) {
|
|
533
|
+
await scanDir(path.join(dirPath, entry.name), folderRelPath, nextDepth);
|
|
566
534
|
}
|
|
567
535
|
}
|
|
568
536
|
}
|
|
569
|
-
catch (error) {
|
|
570
|
-
console.error(`Error scanning folder ${dirPath}:`, error);
|
|
571
|
-
}
|
|
572
537
|
}
|
|
573
|
-
scanDir(getNotesPath(), '', 0);
|
|
538
|
+
await scanDir(getNotesPath(), '', 0);
|
|
574
539
|
return folders;
|
|
575
540
|
}
|
|
576
541
|
/**
|
|
577
|
-
* Extract all unique tags from local notes
|
|
542
|
+
* Extract all unique tags from local notes.
|
|
543
|
+
*
|
|
544
|
+
* Preferred path: a single `/notes/tags` request to the bridge. NotePlan
|
|
545
|
+
* iterates its own files (no TCC, no per-file HTTP overhead) and returns
|
|
546
|
+
* the deduped, hierarchy-expanded tag list using its native parser.
|
|
547
|
+
*
|
|
548
|
+
* Second path: ripgrep `--only-matching` over Notes/ + Calendar/ — works
|
|
549
|
+
* when running outside the bridge but is unreliable when the calling
|
|
550
|
+
* process lacks Full Disk Access (ripgrep gets interrupted by TCC).
|
|
551
|
+
*
|
|
552
|
+
* Last-resort fallback: read every note via the bridge / fs and extract
|
|
553
|
+
* tags individually. This was the only path before Phase 2c; ~67s on a
|
|
554
|
+
* 5700-note vault.
|
|
578
555
|
*/
|
|
579
|
-
export function extractAllTags() {
|
|
556
|
+
export async function extractAllTags() {
|
|
557
|
+
const bridge = await getBridgeClient();
|
|
558
|
+
if (bridge) {
|
|
559
|
+
try {
|
|
560
|
+
return (await bridge.tags()).sort();
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
// Older NotePlan build without the /notes/tags endpoint, or other
|
|
564
|
+
// transient failure — drop through to the slower paths.
|
|
565
|
+
}
|
|
566
|
+
}
|
|
580
567
|
const tags = new Set();
|
|
581
|
-
|
|
568
|
+
if (await isRipgrepAvailable()) {
|
|
569
|
+
const matches = await ripgrepOnlyMatching(String.raw `[@#][\w/-]+(\([^)]*\))?`, [getNotesPath(), getCalendarPath()]);
|
|
570
|
+
if (matches !== null) {
|
|
571
|
+
for (const tag of extractTagsFromContent(matches.join('\n'))) {
|
|
572
|
+
tags.add(tag);
|
|
573
|
+
}
|
|
574
|
+
return Array.from(tags).sort();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const [projectNotes, calendarNotes] = await Promise.all([
|
|
578
|
+
listProjectNotes(),
|
|
579
|
+
listCalendarNotes(),
|
|
580
|
+
]);
|
|
581
|
+
const notes = [...projectNotes, ...calendarNotes];
|
|
582
582
|
for (const note of notes) {
|
|
583
583
|
for (const tag of extractTagsFromContent(note.content)) {
|
|
584
584
|
tags.add(tag);
|
|
@@ -587,21 +587,19 @@ export function extractAllTags() {
|
|
|
587
587
|
return Array.from(tags).sort();
|
|
588
588
|
}
|
|
589
589
|
/**
|
|
590
|
-
* Search notes by content
|
|
590
|
+
* Search notes by content.
|
|
591
591
|
*/
|
|
592
|
-
export function searchLocalNotes(query, options = {}) {
|
|
592
|
+
export async function searchLocalNotes(query, options = {}) {
|
|
593
593
|
const { types, folder, limit = 50 } = options;
|
|
594
594
|
const results = [];
|
|
595
595
|
const lowerQuery = query.toLowerCase();
|
|
596
|
-
// Get notes to search
|
|
597
596
|
let notes = [];
|
|
598
597
|
if (!types || types.includes('note')) {
|
|
599
|
-
notes.push(...listProjectNotes(folder));
|
|
598
|
+
notes.push(...(await listProjectNotes(folder)));
|
|
600
599
|
}
|
|
601
600
|
if (!types || types.includes('calendar')) {
|
|
602
|
-
notes.push(...listCalendarNotes());
|
|
601
|
+
notes.push(...(await listCalendarNotes()));
|
|
603
602
|
}
|
|
604
|
-
// Search
|
|
605
603
|
for (const note of notes) {
|
|
606
604
|
if (note.content.toLowerCase().includes(lowerQuery) ||
|
|
607
605
|
note.title.toLowerCase().includes(lowerQuery)) {
|